diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 791367a3..9c4ebc20 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,7 +9,7 @@ body: value: | 感谢您花时间填写此 Bug 报告! - - **提交错误报告前**:请检查 [已有 Issues](https://github.com/linux-do/pay/issues)列表,了解是否有类似问题被报告。如果不确定,请进行搜索,这有助于我们高效地专注于改进项目。 + - **提交错误报告前**:请检查 [已有 Issues](https://github.com/linux-do/credit/issues)列表,了解是否有类似问题被报告。如果不确定,请进行搜索,这有助于我们高效地专注于改进项目。 - type: checkboxes id: issue-check attributes: @@ -18,7 +18,7 @@ body: options: - label: 我已经搜索了现有问题和讨论。 required: true - - label: 我正在使用最新版本或 cdk.linux.do 上的此应用。 + - label: 我正在使用最新版本或 credit.linux.do 上的此应用。 required: true - type: textarea id: what-happened @@ -68,7 +68,7 @@ body: options: - label: 我已阅读并遵循了 `README.md` 中的所有说明。 required: true - - label: 我正在使用 linux-do/cdk 的最新版本。 + - label: 我正在使用 linux-do/credit 的最新版本。 required: true - label: 我已提供我能够提供的尽可能多的相关日志,屏幕截图等。 required: true @@ -87,4 +87,4 @@ body: value: | ## 注意 如果 Bug 报告不完整或不遵循说明,则可能不会得到处理。请确保您已遵循所有 **README.md** 指南,并提供所有必要信息以便我们重现该问题。 - 感谢您为 linux-do/cdk 做出贡献! + 感谢您为 linux-do/credit 做出贡献! diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f52668e6..7faa5ea3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -9,7 +9,7 @@ body: value: | 感谢您花时间填写此功能请求! - - **提交功能请求前**:请检查 [已有 Issues](https://github.com/linux-do/pay/issues) 列表和讨论区,了解是否有类似功能已被讨论或请求。这有助于我们避免重复工作,并高效地专注于改进项目。 + - **提交功能请求前**:请检查 [已有 Issues](https://github.com/linux-do/credit/issues) 列表和讨论区,了解是否有类似功能已被讨论或请求。这有助于我们避免重复工作,并高效地专注于改进项目。 - type: checkboxes id: check-existing attributes: @@ -18,7 +18,7 @@ body: options: - label: 我已经搜索了现有问题和讨论。 required: true - - label: 我正在使用最新版本或 cdk.linux.do 上的此应用。 + - label: 我正在使用最新版本或 credit.linux.do 上的此应用。 required: true - type: textarea id: feature-description @@ -65,7 +65,7 @@ body: options: - label: 我已阅读并遵循了 `README.md` 中的所有说明。 required: true - - label: 我正在使用 linux-do/cdk 的最新版本。 + - label: 我正在使用 linux-do/credit 的最新版本。 required: true - label: 我已提供我能够提供的尽可能多的相关信息,包括用例和场景。 required: true @@ -76,4 +76,4 @@ body: value: | ## 注意 如果功能请求不完整或不遵循说明,则可能不会得到处理。请确保您已提供所有必要信息以便我们理解您的建议。 - 感谢您为 linux-do/cdk 做出贡献! + 感谢您为 linux-do/credit 做出贡献! diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e74543f0..efad130f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,9 +2,9 @@ -- [ ] 我已阅读并理解 [贡献者公约](https://github.com/linux-do/pay/blob/master/CODE_OF_CONDUCT.md), -- [ ] 我已阅读并同意 [贡献者许可协议 (CLA)](https://github.com/linux-do/pay/blob/master/CLA.md),确认我的贡献将根据项目的 MIT 许可证进行许可, -- [ ] 我知晓如果此 PR 并不做出实质性更改,或可被认为是*为了PR被合并而提交PR*的,则可能不会被合并, +- [ ] 我已阅读并理解 [贡献者公约](https://github.com/linux-do/credit/blob/master/CODE_OF_CONDUCT.md) +- [ ] 我已阅读并同意 [贡献者许可协议 (CLA)](https://github.com/linux-do/credit/blob/master/CLA.md),确认我的贡献将根据项目的 Apache2.0 许可证进行许可 +- [ ] 我知晓如果此 PR 并不做出实质性更改,或可被认为是*为了PR被合并而提交PR*的,则可能不会被合并 **关联信息** diff --git a/.github/workflows/build_backend.yml b/.github/workflows/build_backend.yml index dd718b8f..5d88d96d 100644 --- a/.github/workflows/build_backend.yml +++ b/.github/workflows/build_backend.yml @@ -10,7 +10,7 @@ on: jobs: go_build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v4 @@ -18,12 +18,13 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25.4" + go-version: "1.26" + check-latest: true - name: Install dependencies run: | go mod download - go install github.com/swaggo/swag/cmd/swag@latest + go install github.com/swaggo/swag/cmd/swag@v1.16.6 - name: Make Swagger run: make swagger diff --git a/.github/workflows/build_frontend.yml b/.github/workflows/build_frontend.yml index eac227bb..22e8d969 100644 --- a/.github/workflows/build_frontend.yml +++ b/.github/workflows/build_frontend.yml @@ -10,27 +10,27 @@ on: jobs: nextjs_build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10.10.0 - + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "20" cache: 'pnpm' cache-dependency-path: frontend/pnpm-lock.yaml - + - name: Install dependencies working-directory: frontend run: pnpm install --frozen-lockfile - + - name: Build working-directory: frontend run: pnpm build diff --git a/.github/workflows/build_image.yml b/.github/workflows/build_image.yml index 2ceeb4dd..29ad9cb6 100644 --- a/.github/workflows/build_image.yml +++ b/.github/workflows/build_image.yml @@ -10,6 +10,7 @@ on: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} + FRONTEND_IMAGE_NAME: ${{ github.repository }}-web # add permission for package publishing permissions: @@ -18,12 +19,19 @@ permissions: jobs: build_image: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: # checkout code - name: Checkout repository uses: actions/checkout@v4 + # set up go + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26" + check-latest: true + # download Go modules - name: Download Go modules run: go mod download @@ -32,13 +40,13 @@ jobs: - name: Build Go Binary (multi-arch) run: | # build amd64 binary - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o pay-server-amd64 main.go + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o credit-server-amd64 main.go # build arm64 binary - GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o pay-server-arm64 main.go + GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o credit-server-arm64 main.go # show binaries - ls -alh pay-server-* + ls -alh credit-server-* # set up docker buildx - name: Set up Docker Buildx @@ -74,3 +82,119 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 + + build_frontend_image: + runs-on: ubuntu-24.04 + steps: + # checkout code + - name: Checkout repository + uses: actions/checkout@v4 + + # set up docker buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # login to registry + - name: Log into registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # extract metadata for docker (frontend) + - name: Extract Docker metadata (frontend) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }} + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=tag + type=sha,format=short,prefix= + + # generate frontend build date in utc + - name: Generate frontend build date + id: frontend_build_meta + run: echo "build_date=$(date -u +'%Y/%m/%d UTC')" >> "$GITHUB_OUTPUT" + + # build and push frontend docker image with version argument + - name: Build Frontend Docker image + uses: docker/build-push-action@v5 + with: + context: ./frontend + push: ${{ github.ref_type == 'tag' || (github.ref == 'refs/heads/master' && github.event_name == 'push') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + build-args: | + VERSION=${{ github.ref_type == 'tag' && github.ref_name || '' }} + BUILD_DATE=${{ steps.frontend_build_meta.outputs.build_date }} + + update_helm_values: + runs-on: ubuntu-24.04 + needs: [build_image, build_frontend_image] + if: github.ref_type == 'tag' + steps: + # configure SSH agent with deploy key + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.DEPLOY_KEY }} + + # add github.com to known_hosts to avoid host verification prompt + - name: Add GitHub to known_hosts + run: ssh-keyscan github.com >> ~/.ssh/known_hosts + + # clone the helm charts repository + - name: Clone Helm Charts repository + run: git clone ${{ vars.HELM_CHARTS_REPO }} helm-charts + + # install yq for yaml editing + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + # determine helm values file based on tag type (production or test) + - name: Determine Helm values file + id: helm_config + run: | + TAG="${{ github.ref_name }}" + if [[ $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "values_file=values/credit/credit.yaml" >> "$GITHUB_OUTPUT" + echo "branch_prefix=update/credit" >> "$GITHUB_OUTPUT" + else + echo "values_file=values/credit_test/credit.yaml" >> "$GITHUB_OUTPUT" + echo "branch_prefix=update/credit_test" >> "$GITHUB_OUTPUT" + fi + + # update image tags in helm values file + - name: Update image tags in helm values file + run: | + cd helm-charts + yq -i '.image.tag = "${{ github.ref_name }}"' ${{ steps.helm_config.outputs.values_file }} + yq -i '.image.webTag = "${{ github.ref_name }}"' ${{ steps.helm_config.outputs.values_file }} + + # commit and push to a new branch, then open a PR for review + - name: Commit, push and create PR + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + cd helm-charts + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + BRANCH_NAME="${{ steps.helm_config.outputs.branch_prefix }}-${{ github.ref_name }}" + git checkout -b "$BRANCH_NAME" + git add ${{ steps.helm_config.outputs.values_file }} + git commit -m "chore: update credit image tag to ${{ github.ref_name }}" + git push origin "$BRANCH_NAME" + HELM_REPO=$(echo "${{ vars.HELM_CHARTS_REPO }}" | sed 's/git@github.com://;s/\.git$//') + gh pr create \ + --repo "${HELM_REPO}" \ + --title "chore: update credit image tag to ${{ github.ref_name }}" \ + --body "Automated PR: update \`${{ steps.helm_config.outputs.values_file }}\` image tags to \`${{ github.ref_name }}\` after Docker image build." \ + --base main \ + --head "$BRANCH_NAME" diff --git a/.github/workflows/check_license.yml b/.github/workflows/check_license.yml index 25c0cc5e..b625d9a6 100644 --- a/.github/workflows/check_license.yml +++ b/.github/workflows/check_license.yml @@ -9,7 +9,7 @@ on: jobs: check_license: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/close_ticket.yml b/.github/workflows/close_ticket.yml index fcacf28b..4ebf1f4a 100644 --- a/.github/workflows/close_ticket.yml +++ b/.github/workflows/close_ticket.yml @@ -6,7 +6,7 @@ on: jobs: close_ticket: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write pull-requests: write diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 16edd5ec..7d76f9c8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -11,7 +11,7 @@ on: jobs: analyze: name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: security-events: write packages: read diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 69b36659..4aec2afc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -11,7 +11,7 @@ on: jobs: copilot-setup-steps: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read @@ -39,9 +39,10 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version: "1.26" + check-latest: true - name: Install dependencies run: | go mod download - go install github.com/swaggo/swag/cmd/swag@latest + go install github.com/swaggo/swag/cmd/swag@v1.16.6 diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 9f959cbe..f8865db9 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -10,7 +10,7 @@ on: jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pr-template-check.yml b/.github/workflows/pr-template-check.yml index 16ea9467..e61365de 100644 --- a/.github/workflows/pr-template-check.yml +++ b/.github/workflows/pr-template-check.yml @@ -6,7 +6,7 @@ on: jobs: check-pr-template: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check all checklist items are checked uses: actions/github-script@v7 diff --git a/.gitignore b/.gitignore index 0c019aef..b2cd718b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ # IDE .idea/* +.idea !.idea/icon.png .vscode +.pnpm-store/ # logs logs @@ -34,5 +36,12 @@ Thumbs.db # go workspace go.work go.work.sum -cdk +/credit main + +# upload +uploads/* + +s3_cache + +.gocache \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4635e05f..32ed842f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ -FROM alpine:latest +FROM alpine:3.23 # set the time zone to Beijing Time in the Eastern 8th Time Zone +ARG TZ=Asia/Shanghai RUN apk add --no-cache tzdata && \ - cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ - echo "Asia/Shanghai" > /etc/timezone + cp /usr/share/zoneinfo/${TZ} /etc/localtime && \ + echo "${TZ}" > /etc/timezone WORKDIR /app @@ -11,7 +12,7 @@ WORKDIR /app ARG TARGETPLATFORM # copy the appropriate binary based on platform -COPY pay-server-${TARGETPLATFORM#linux/} ./pay-server +COPY credit-server-${TARGETPLATFORM#linux/} ./credit-server # copy docs COPY docs ./docs @@ -19,4 +20,4 @@ COPY docs ./docs EXPOSE 8000 # set entrypoint -ENTRYPOINT ["./pay-server"] +ENTRYPOINT ["./credit-server"] diff --git a/README.md b/README.md index b95f998f..18fdbff1 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,40 @@ # LINUX DO Credit -🚀 Linux Do 社区 Credit (Content Distribution Kit) 快速分享平台 +🚀 Linux Do Community Credit Service Platform -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Go Version](https://img.shields.io/badge/Go-1.24-blue.svg)](https://golang.org/) +[中文](./README_zh.md) + +[![License: Apache2.0](https://img.shields.io/badge/License-Apache2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Go Version](https://img.shields.io/badge/Go-1.26-blue.svg)](https://golang.org/) [![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)](https://nextjs.org/) [![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/) -[![GitHub release](https://img.shields.io/github/v/release/linux-do/pay?include_prereleases)](https://github.com/linux-do/pay/releases) -[![GitHub stars](https://img.shields.io/github/stars/linux-do/pay)](https://github.com/linux-do/pay/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/linux-do/pay)](https://github.com/linux-do/pay/network) -[![GitHub issues](https://img.shields.io/github/issues/linux-do/pay)](https://github.com/linux-do/pay/issues) -[![GitHub pull requests](https://img.shields.io/github/issues-pr/linux-do/pay)](https://github.com/linux-do/pay/pulls) -[![GitHub contributors](https://img.shields.io/github/contributors/linux-do/pay)](https://github.com/linux-do/pay/graphs/contributors) +[![GitHub release](https://img.shields.io/github/v/release/linux-do/credit?include_prereleases)](https://github.com/linux-do/credit/releases) +[![GitHub stars](https://img.shields.io/github/stars/linux-do/credit)](https://github.com/linux-do/credit/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/linux-do/credit)](https://github.com/linux-do/credit/network) +[![GitHub issues](https://img.shields.io/github/issues/linux-do/credit)](https://github.com/linux-do/credit/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/linux-do/credit)](https://github.com/linux-do/credit/pulls) +[![GitHub contributors](https://img.shields.io/github/contributors/linux-do/credit)](https://github.com/linux-do/credit/graphs/contributors) + +[![Backend Build](https://github.com/linux-do/credit/actions/workflows/build_backend.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/build_backend.yml) +[![Frontend Build](https://github.com/linux-do/credit/actions/workflows/build_frontend.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/build_frontend.yml) +[![Docker Build](https://github.com/linux-do/credit/actions/workflows/build_image.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/build_image.yml) +[![CodeQL](https://github.com/linux-do/credit/actions/workflows/codeql.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/codeql.yml) +[![ESLint](https://github.com/linux-do/credit/actions/workflows/eslint.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/eslint.yml) -[![Backend Build](https://github.com/linux-do/pay/actions/workflows/build_backend.yml/badge.svg)](https://github.com/linux-do/pay/actions/workflows/build_backend.yml) -[![Frontend Build](https://github.com/linux-do/pay/actions/workflows/build_frontend.yml/badge.svg)](https://github.com/linux-do/pay/actions/workflows/build_frontend.yml) -[![Docker Build](https://github.com/linux-do/pay/actions/workflows/build_image.yml/badge.svg)](https://github.com/linux-do/pay/actions/workflows/build_image.yml) -[![CodeQL](https://github.com/linux-do/pay/actions/workflows/codeql.yml/badge.svg)](https://github.com/linux-do/pay/actions/workflows/codeql.yml) -[![ESLint](https://github.com/linux-do/pay/actions/workflows/eslint.yml/badge.svg)](https://github.com/linux-do/pay/actions/workflows/eslint.yml) +## 📖 Introduction -## 📖 项目简介 +LINUX DO Credit is a credit service platform built for the Linux Do community, aimed at providing a series of credit-related services and offering a foundational framework for credit circulation for community developers. -LINUX DO Credit 是一个为 Linux Do 社区打造的积分服务平台,旨在提供一系列积分相关服务,为社区开发者提供积分流转基础框架。 -### ✨ 主要特性 +### ✨ Key Features -- 🔐 **OAuth2 认证** - 集成 Linux Do 社区账号系统 -- 🛡️ **风险控制** - 完善的信任等级和风险评估系统 -- 📊 **实时监控** - 详细的分发统计和用户行为分析 -- 🎨 **现代化界面** - 基于 Next.js 16 和 React 19 的响应式设计 -- ⚡ **高性能** - Go 后端 + Redis 缓存 + PostgreSQL 数据库 +- 🔐 **OAuth2 Authentication** - Integrated with Linux Do community account system +- 🛡️ **Risk Control** - Comprehensive trust level and risk assessment system +- 📊 **Real-time Monitoring** - Detailed distribution statistics and user behavior analysis +- 🎨 **Modern Interface** - Responsive design based on Next.js 16 and React 19 +- ⚡ **High Performance** - Go Backend + Redis Cache + PostgreSQL Database -## 🏗️ 架构概览 +## 🏗️ Architecture Overview ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ @@ -40,34 +43,245 @@ LINUX DO Credit 是一个为 Linux Do 社区打造的积分服务平台,旨在 │ │ │ │ │ │ │ • React 19 │ │ • Gin Framework │ │ • PostgreSQL │ │ • TypeScript │ │ • OAuth2 │ │ • Redis Cache │ -│ • Tailwind CSS │ │ • OpenTelemetry │ │ • Session Store │ -│ • Shadcn UI │ │ • Swagger API │ │ │ +│ • Tailwind CSS │ │ • Session Store │ │ │ +│ • Shadcn UI │ │ • OpenTelemetry │ │ │ +│ │ │ • Swagger API │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` -## 🛠️ 技术栈 +## 🛠️ Tech Stack -### 后端 -- **Go 1.25.4** - 主要开发语言 -- **Gin** - Web 框架 -- **GORM** - ORM 框架 -- **Redis** - 缓存和会话存储 -- **PostgreSQL** - 主数据库 -- **OpenTelemetry** - 可观测性 -- **Swagger** - API 文档 +### Backend +- **[Go 1.26](https://go.dev/doc)** - Primary development language +- **[GIN](https://github.com/gin-gonic/gin)** - Web Framework +- **[GORM](https://github.com/go-gorm/gorm)** - ORM Framework +- **[Redis](https://github.com/redis/redis)** - Cache and session store +- **[PostgreSQL](https://www.postgresql.org)** - Primary Database +- **[OpenTelemetry](https://opentelemetry.io)** - Observability +- **[Swagger](https://github.com/swaggo/swag)** - API Documentation -### 前端 -- **Next.js 16** - React 框架 -- **React 19** - UI 库 -- **TypeScript** - 类型安全 -- **Tailwind CSS 4** - 样式框架 -- **Shadcn UI** - 组件库 -- **Lucide Icons** - 图标库 +### Frontend +- **[Next.js 16](https://github.com/vercel/next.js)** - React Framework +- **[React 19](https://github.com/facebook/react)** - UI Library +- **[TypeScript](https://github.com/microsoft/TypeScript)** - Type Safety +- **[Tailwind CSS 4](https://github.com/tailwindlabs/tailwindcss)** - Styling Framework +- **[Shadcn UI](https://github.com/shadcn-ui/ui)** - Component Library +- **[Lucide Icons](https://github.com/lucide-icons/lucide)** - Icon Library -## 📋 环境要求 +## 📋 Requirements -- **Go** >= 1.25.4 +- **Go** >= 1.26 - **Node.js** >= 18.0 - **PostgreSQL** >= 18 - **Redis** >= 6.0 -- **pnpm** >= 8.0 (推荐) \ No newline at end of file +- **pnpm** >= 8.0 (Recommended) + +## 🚀 Quick Start + +### 1. Clone the Project + +```bash +git clone https://github.com/linux-do/credit.git +cd credit +``` + +### 2. Configure Environment + +Copy the configuration file and edit it: + +```bash +cp config.example.yaml config.yaml +``` + +Edit `config.yaml` to configure database connections, Redis, OAuth2, etc. + +### 3. Initialize Database + +```bash +# Create database +createdb -h -p 5432 -U postgres linux_do_credit + +# If you need to specify encoding, use: +# psql -h -p 5432 -U postgres -c "CREATE DATABASE linux_do_credit WITH ENCODING 'UTF8' LC_COLLATE='zh_CN.UTF-8' LC_CTYPE='zh_CN.UTF-8' TEMPLATE template0;" + +# Run migrations (automatically executed when starting the backend) +``` + +### 4. Start Backend + +```bash +# Install Go dependencies +go mod tidy + +# Generate API documentation +make swagger + +# Start backend service +go run main.go api +``` + +### 5. Start Frontend + +```bash +cd frontend + +# Install dependencies +pnpm install + +# Start development server +pnpm dev +``` + +### 6. Access Application + +- **Frontend Interface**: http://localhost:3000 +- **API Documentation**: http://localhost:8000/swagger/index.html +- **Health Check**: http://localhost:8000/api/v1/health +- **Readiness Check**: http://localhost:8000/api/v1/ready + +## ⚙️ Configuration + +### Main Configuration Options + +| Option | Description | Example | +|--------|-------------|---------| +| `app.addr` | Backend service listening address | `:8000` | +| `worker.port` | Worker probe port | `8001` | +| `scheduler.port` | Scheduler probe port | `8002` | +| `oauth2.client_id` | OAuth2 Client ID | `your_client_id` | +| `database.host` | PostgreSQL database host | `127.0.0.1` | +| `database.port` | PostgreSQL database port | `5432` | +| `database.username` | PostgreSQL database username | `postgres` | +| `database.password` | PostgreSQL database password | `password` | +| `database.database` | PostgreSQL database name | `linux_do_credit` | +| `database.ssl_mode` | PostgreSQL SSL mode | `disable` | +| `database.application_name` | PostgreSQL application name | `credit-server` | +| `database.search_path` | PostgreSQL search path | `public` | +| `database.default_query_exec_mode` | SQL cache mode | `cache_statement` | +| `redis.host` | Redis server address | `127.0.0.1` | + +For detailed configuration instructions, please refer to the `config.example.yaml` file. + +## 🔧 Development Guide + +### Backend Development + +```bash +# Run API server +go run main.go api + +# Run task scheduler +go run main.go scheduler + +# Run worker queue +go run main.go worker + +# Generate Swagger documentation +make swagger + +# Format and check code +make tidy +``` + +### Frontend Development + +```bash +cd frontend + +# Development mode (using Turbopack) +pnpm dev + +# Build production version +pnpm build + +# Start production service +pnpm start + +# Lint and format code +pnpm lint +pnpm format +``` + +## 📚 API Documentation + +API documentation is automatically generated by Swagger and can be accessed after starting the backend service: + +``` +http://localhost:8000/swagger/index.html +``` + +## 🧪 Testing + +```bash +# Backend testing +go test ./... + +# Frontend testing +cd frontend +pnpm test +``` + +## 🚀 Deployment + +### Docker Deployment + +```bash +# Build image +docker build -t linux-do-credit . + +# Run container +docker run -d -p 8000:8000 linux-do-credit +``` + +### Production Environment Deployment + +1. Build frontend resources: + ```bash + cd frontend && pnpm build + ``` + +2. Compile backend program: + ```bash + go build -o credit main.go + ``` + +3. Configure `config.yaml` for production + +4. Start service: + ```bash + ./credit api + ``` + +## 🤝 Contribution Guidelines + +We welcome community contributions! Please read the following before submitting code: + +- [Contribution Guidelines](CONTRIBUTING.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Contributor License Agreement](CLA.md) + +### Submission Process + +1. Fork this repository +2. Create a feature branch (`git checkout -b feature/your-feature`) +3. Commit changes (`git commit -am 'Add your feature'`) +4. Push to branch (`git push origin feature/your-feature`) +5. Create Pull Request + +## 📄 License + +This project is open source under the [Apache2.0 License](LICENSE). + +## 🔗 Related Links + +- [Linux Do Community](https://linux.do) +- [Issue Reporting](https://github.com/linux-do/credit/issues) +- [Feature Request](https://github.com/linux-do/credit/issues/new?template=feature_request.md) + +## ❤️ Acknowledgements + +Thanks to all developers who contributed to this project and the support of the Linux Do Community! + +## 📈 Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=linux-do/credit&type=Date)](https://star-history.com/#linux-do/credit&Date) diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 00000000..e5a98768 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,287 @@ +# LINUX DO Credit + +🚀 Linux Do 社区 Credit 积分服务平台 + +[English](./README.md) + +[![License: Apache2.0](https://img.shields.io/badge/License-Apache2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Go Version](https://img.shields.io/badge/Go-1.26-blue.svg)](https://golang.org/) +[![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)](https://nextjs.org/) +[![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/) + +[![GitHub release](https://img.shields.io/github/v/release/linux-do/credit?include_prereleases)](https://github.com/linux-do/credit/releases) +[![GitHub stars](https://img.shields.io/github/stars/linux-do/credit)](https://github.com/linux-do/credit/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/linux-do/credit)](https://github.com/linux-do/credit/network) +[![GitHub issues](https://img.shields.io/github/issues/linux-do/credit)](https://github.com/linux-do/credit/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/linux-do/credit)](https://github.com/linux-do/credit/pulls) +[![GitHub contributors](https://img.shields.io/github/contributors/linux-do/credit)](https://github.com/linux-do/credit/graphs/contributors) + +[![Backend Build](https://github.com/linux-do/credit/actions/workflows/build_backend.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/build_backend.yml) +[![Frontend Build](https://github.com/linux-do/credit/actions/workflows/build_frontend.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/build_frontend.yml) +[![Docker Build](https://github.com/linux-do/credit/actions/workflows/build_image.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/build_image.yml) +[![CodeQL](https://github.com/linux-do/credit/actions/workflows/codeql.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/codeql.yml) +[![ESLint](https://github.com/linux-do/credit/actions/workflows/eslint.yml/badge.svg)](https://github.com/linux-do/credit/actions/workflows/eslint.yml) + +## 📖 项目简介 + +LINUX DO Credit 是一个为 Linux Do 社区打造的积分服务平台,旨在提供一系列积分相关服务,为社区开发者提供积分流转基础框架。 + +### ✨ 主要特性 + +- 🔐 **OAuth2 认证** - 集成 Linux Do 社区账号系统 +- 🛡️ **风险控制** - 完善的信任等级和风险评估系统 +- 📊 **实时监控** - 详细的分发统计和用户行为分析 +- 🎨 **现代化界面** - 基于 Next.js 16 和 React 19 的响应式设计 +- ⚡ **高性能** - Go 后端 + Redis 缓存 + PostgreSQL 数据库 + +## 🏗️ 架构概览 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ Database │ +│ (Next.js) │◄──►│ (Go) │◄──►│ (PostgreSQL) │ +│ │ │ │ │ │ +│ • React 19 │ │ • Gin Framework │ │ • PostgreSQL │ +│ • TypeScript │ │ • OAuth2 │ │ • Redis Cache │ +│ • Tailwind CSS │ │ • Session Store │ │ │ +│ • Shadcn UI │ │ • OpenTelemetry │ │ │ +│ │ │ • Swagger API │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 🛠️ 技术栈 + +### 后端 +- **[Go 1.26](https://go.dev/doc)** - 主要开发语言 +- **[GIN](https://github.com/gin-gonic/gin)** - Web 框架 +- **[GORM](https://github.com/go-gorm/gorm)** - ORM 框架 +- **[Redis](https://github.com/redis/redis)** - 缓存和会话存储 +- **[PostgreSQL](https://www.postgresql.org)** - 主数据库 +- **[OpenTelemetry](https://opentelemetry.io)** - 可观测性 +- **[Swagger](https://github.com/swaggo/swag)** - API 文档 + +### 前端 +- **[Next.js 16](https://github.com/vercel/next.js)** - React 框架 +- **[React 19](https://github.com/facebook/react)** - UI 库 +- **[TypeScript](https://github.com/microsoft/TypeScript)** - 类型安全 +- **[Tailwind CSS 4](https://github.com/tailwindlabs/tailwindcss)** - 样式框架 +- **[Shadcn UI](https://github.com/shadcn-ui/ui)** - 组件库 +- **[Lucide Icons](https://github.com/lucide-icons/lucide)** - 图标库 + +## 📋 环境要求 + +- **Go** >= 1.26 +- **Node.js** >= 18.0 +- **PostgreSQL** >= 18 +- **Redis** >= 6.0 +- **pnpm** >= 8.0 (推荐) + +## 🚀 快速开始 + +### 1. 克隆项目 + +```bash +git clone https://github.com/linux-do/credit.git +cd credit +``` + +### 2. 配置环境 + +复制配置文件并编辑: + +```bash +cp config.example.yaml config.yaml +``` + +编辑 `config.yaml` 文件,配置数据库连接、Redis、OAuth2 等信息。 + +### 3. 初始化数据库 + +```bash +# 创建数据库 +createdb -h <主机> -p 5432 -U postgres linux_do_credit + +# 如果需要指定字符集,可使用 +# psql -h <主机> -p 5432 -U postgres -c "CREATE DATABASE linux_do_credit WITH ENCODING 'UTF8' LC_COLLATE='zh_CN.UTF-8' LC_CTYPE='zh_CN.UTF-8' TEMPLATE template0;" + +# 运行迁移(启动后端时会自动执行) +``` + +### 4. 启动后端 + +```bash +# 安装 Go 依赖 +go mod tidy + +# 生成 API 文档 +make swagger + +# 启动后端服务 +go run main.go api +``` + +### 5. 启动前端 + +```bash +cd frontend + +# 安装依赖 +pnpm install + +# 启动开发服务器 +pnpm dev +``` + +### 6. 访问应用 + +- **前端界面**: http://localhost:3000 +- **API 文档**: http://localhost:8000/swagger/index.html +- **健康检查**: http://localhost:8000/api/v1/health +- **就绪检查**: http://localhost:8000/api/v1/ready + +## ⚙️ 配置说明 + +### 主要配置项 + +| 配置项 | 说明 | 示例 | +|--------|------|------| +| `app.addr` | 后端服务监听地址 | `:8000` | +| `worker.port` | Worker 探针端口 | `8001` | +| `scheduler.port` | Scheduler 探针端口 | `8002` | +| `oauth2.client_id` | OAuth2 客户端 ID | `your_client_id` | +| `database.host` | PostgreSQL 数据库地址 | `127.0.0.1` | +| `database.port` | PostgreSQL 数据库端口 | `5432` | +| `database.username` | PostgreSQL 数据库用户名 | `postgres` | +| `database.password` | PostgreSQL 数据库密码 | `password` | +| `database.database` | PostgreSQL 数据库名称 | `linux_do_credit` | +| `database.ssl_mode` | PostgreSQL SSL 模式 | `disable` | +| `database.application_name` | PostgreSQL 应用标识 | `credit-server` | +| `database.search_path` | PostgreSQL 搜索路径 | `public` | +| `database.default_query_exec_mode` | SQL 缓存模式 | `cache_statement` | +| `redis.host` | Redis 服务器地址 | `127.0.0.1` | + +详细配置说明请参考 `config.example.yaml` 文件。 + +## 🔧 开发指南 + +### 后端开发 + +```bash +# 运行 API 服务器 +go run main.go api + +# 运行任务调度器 +go run main.go scheduler + +# 运行工作队列 +go run main.go worker + +# 生成 Swagger 文档 +make swagger + +# 代码格式化和检查 +make tidy +``` + +### 前端开发 + +```bash +cd frontend + +# 开发模式(使用 Turbopack) +pnpm dev + +# 构建生产版本 +pnpm build + +# 启动生产服务 +pnpm start + +# 代码检查和格式化 +pnpm lint +pnpm format +``` + +## 📚 API 文档 + +API 文档通过 Swagger 自动生成,启动后端服务后可访问: + +``` +http://localhost:8000/swagger/index.html +``` + +## 🧪 测试 + +```bash +# 后端测试 +go test ./... + +# 前端测试 +cd frontend +pnpm test +``` + +## 🚀 部署 + +### Docker 部署 + +```bash +# 构建镜像 +docker build -t linux-do-credit . + +# 运行容器 +docker run -d -p 8000:8000 linux-do-credit +``` + +### 生产环境部署 + +1. 构建前端资源: + ```bash + cd frontend && pnpm build + ``` + +2. 编译后端程序: + ```bash + go build -o credit main.go + ``` + +3. 配置生产环境的 `config.yaml` + +4. 启动服务: + ```bash + ./credit api + ``` + +## 🤝 贡献指南 + +我们欢迎社区贡献!请在提交代码前阅读: + +- [贡献指南](CONTRIBUTING.md) +- [行为准则](CODE_OF_CONDUCT.md) +- [贡献者许可协议](CLA.md) + +### 提交流程 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/your-feature`) +3. 提交更改 (`git commit -am 'Add your feature'`) +4. 推送到分支 (`git push origin feature/your-feature`) +5. 创建 Pull Request + +## 📄 许可证 + +本项目基于 [Apache2.0 许可证](LICENSE) 开源。 + +## 🔗 相关链接 + +- [Linux Do 社区](https://linux.do) +- [问题反馈](https://github.com/linux-do/credit/issues) +- [功能请求](https://github.com/linux-do/credit/issues/new?template=feature_request.md) + +## ❤️ 致谢 + +感谢所有为本项目做出贡献的开发者和 Linux Do 社区的支持! + +## 📈 项目趋势 + +[![Star History Chart](https://api.star-history.com/svg?repos=linux-do/credit&type=Date)](https://star-history.com/#linux-do/credit&Date) diff --git a/config.example.yaml b/config.example.yaml index eade8fc0..945c4139 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,6 +6,7 @@ app: app_name: "linux-do-credit" env: "development" # development, testing, production addr: ":8000" + node_id: 1 # 分布式节点 ID (0-1023),不同实例必须不同 graceful_shutdown_timeout: 30 session_cookie_name: "linux_do_credit_session_id" # change this in local dev env session_secret: "" # you can't change this after first time start @@ -14,13 +15,15 @@ app: session_secure: false session_http_only: false api_prefix: "/api" + frontend_url: "http://localhost:3000" frontend_pay_url: "http://localhost:3000/paying" -# OAuth2 +# OAuth2/OIDC(优先) oauth2: client_id: "" client_secret: "" redirect_uri: "" + issuer: "https://connect.linux.do/" # OIDC Issuer URL,用于自动发现端点 authorization_endpoint: "https://connect.linux.do/oauth2/authorize" token_endpoint: "https://connect.linux.do/oauth2/token" user_endpoint: "https://connect.linux.do/api/user" @@ -47,12 +50,27 @@ database: search_path: "public" statement_cache_capacity: 256 default_query_exec_mode: "cache_statement" + slow_threshold: 200ms # replicas: # - host: "replica1.db.local" # port: 5432 # - host: "replica2.db.local" # port: 5432 +# clickhouse +clickhouse: + enabled: false + hosts: + - "127.0.0.1:9000" + username: "default" + password: "" + database: "linux_do_credit" + max_idle_conn: 10 + max_open_conn: 100 + conn_max_lifetime: 3600 + dial_timeout: 5 + block_buffer_size: 10 + # Redis # 支持三种模式:Standalone(单节点)、Sentinel(高可用)、Cluster(水平扩展) redis: @@ -85,15 +103,20 @@ log: max_backups: 10 compress: true -# Schedule -schedule: - user_gamification_score_dispatch_interval_seconds: 3 +# Scheduler +scheduler: + port: 8002 # scheduler probe port update_user_gamification_scores_task_cron: "0 2 * * *" dispute_auto_refund_dispatch_interval_seconds: 3 auto_refund_expired_disputes_task_cron: "0 0 * * *" + sync_orders_to_clickhouse_task_cron: "10 0 * * *" + refund_expired_red_envelopes_task_cron: "0 1 * * *" + cleanup_unused_uploads_task_cron: "0 */2 * * *" + settle_pending_payments_task_cron: "0 * * * *" # Worker worker: + port: 8001 # worker probe port concurrency: 20 strict_priority: false queues: @@ -103,11 +126,41 @@ worker: priority: 5 - name: default priority: 3 + # 积分更新速率限制:rate 次/period 秒 + gamification_score_rate_limit: + rate: 1 # 允许的请求次数 + period: 3 # 时间周期(秒) # linuxDo linuxDo: api_key: "" +# OpenAPI Risk +openapi_risk: + enabled: false + base_url: "https://audit.example.com" + username: "" + password: "" + cache_ttl_seconds: 3600 + prompt_risk_levels: [] + block_risk_levels: [] + # OpenTelemetry otel: sampling_rate: 0.1 # 采样率 0.0-1.0 + +# S3 Compatible Storage +# 支持 AWS S3、MinIO、Cloudflare R2、腾讯 COS 等 S3 兼容存储 +s3: + enabled: true + endpoint: "https://.r2.cloudflarestorage.com" + region: "auto" + bucket: "" + access_key_id: "" + secret_access_key: "" + path_style: false # MinIO 等自托管服务设为 true + key_prefix: "" # 对象 key 前缀,如 "uploads/",可用于分目录存储 + cdn_url: "" # CDN 域名(如 https://cdn.example.com),为空则直接读 S3 + local_cache: + enabled: false + cache_dir: "./s3_cache" diff --git a/docs/docs.go b/docs/docs.go index 466e3a03..cbb8e673 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -39,19 +39,16 @@ const docTemplate = `{ "required": true }, { + "maxLength": 64, + "minLength": 1, "type": "string", "name": "out_trade_no", - "in": "query" - }, - { - "type": "string", - "name": "pid", "in": "query", "required": true }, { - "type": "integer", - "name": "trade_no", + "type": "string", + "name": "pid", "in": "query", "required": true } @@ -96,23 +93,7 @@ const docTemplate = `{ } } }, - "/api/v1/admin/system-configs": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/util.ResponseAny" - } - } - } - }, + "/api/v1/admin/orders": { "post": { "consumes": [ "application/json" @@ -130,7 +111,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/system_config.CreateSystemConfigRequest" + "$ref": "#/definitions/order.listOrdersRequest" } } ], @@ -144,33 +125,8 @@ const docTemplate = `{ } } }, - "/api/v1/admin/system-configs/{key}": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "parameters": [ - { - "type": "string", - "description": "配置键", - "name": "key", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/util.ResponseAny" - } - } - } - }, - "put": { + "/api/v1/admin/orders/{id}/refund": { + "post": { "consumes": [ "application/json" ], @@ -182,9 +138,9 @@ const docTemplate = `{ ], "parameters": [ { - "type": "string", - "description": "配置键", - "name": "key", + "type": "integer", + "description": "订单ID", + "name": "id", "in": "path", "required": true }, @@ -192,36 +148,10 @@ const docTemplate = `{ "description": "request body", "name": "request", "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/system_config.UpdateSystemConfigRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", "schema": { - "$ref": "#/definitions/util.ResponseAny" + "$ref": "#/definitions/order.refundOrderRequest" } } - } - }, - "delete": { - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "parameters": [ - { - "type": "string", - "description": "配置键", - "name": "key", - "in": "path", - "required": true - } ], "responses": { "200": { @@ -233,7 +163,7 @@ const docTemplate = `{ } } }, - "/api/v1/admin/user-pay-configs": { + "/api/v1/admin/system-configs": { "get": { "produces": [ "application/json" @@ -267,7 +197,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user_pay_config.CreateUserPayConfigRequest" + "$ref": "#/definitions/system_config.CreateSystemConfigRequest" } } ], @@ -281,7 +211,7 @@ const docTemplate = `{ } } }, - "/api/v1/admin/user-pay-configs/{id}": { + "/api/v1/admin/system-configs/{key}": { "get": { "produces": [ "application/json" @@ -292,8 +222,8 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "配置ID", - "name": "id", + "description": "配置键", + "name": "key", "in": "path", "required": true } @@ -320,8 +250,8 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "配置ID", - "name": "id", + "description": "配置键", + "name": "key", "in": "path", "required": true }, @@ -331,7 +261,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user_pay_config.UpdateUserPayConfigRequest" + "$ref": "#/definitions/system_config.UpdateSystemConfigRequest" } } ], @@ -354,8 +284,8 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "配置ID", - "name": "id", + "description": "配置键", + "name": "key", "in": "path", "required": true } @@ -370,8 +300,8 @@ const docTemplate = `{ } } }, - "/api/v1/config/public": { - "get": { + "/api/v1/admin/tasks/dispatch": { + "post": { "consumes": [ "application/json" ], @@ -379,7 +309,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "config" + "admin" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/task.DispatchTaskRequest" + } + } ], "responses": { "200": { @@ -391,13 +332,13 @@ const docTemplate = `{ } } }, - "/api/v1/health": { + "/api/v1/admin/tasks/types": { "get": { "produces": [ "application/json" ], "tags": [ - "health" + "admin" ], "responses": { "200": { @@ -409,13 +350,13 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/api-keys": { + "/api/v1/admin/user-pay-configs": { "get": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "responses": { "200": { @@ -434,7 +375,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { @@ -443,7 +384,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/api_key.CreateAPIKeyRequest" + "$ref": "#/definitions/user_pay_config.CreateUserPayConfigRequest" } } ], @@ -457,19 +398,18 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/api-keys/{id}": { + "/api/v1/admin/user-pay-configs/{id}": { "get": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { - "type": "integer", - "format": "int64", - "description": "API Key ID", + "type": "string", + "description": "配置ID", "name": "id", "in": "path", "required": true @@ -492,13 +432,12 @@ const docTemplate = `{ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { - "type": "integer", - "format": "int64", - "description": "API Key ID", + "type": "string", + "description": "配置ID", "name": "id", "in": "path", "required": true @@ -509,7 +448,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/api_key.UpdateAPIKeyRequest" + "$ref": "#/definitions/user_pay_config.UpdateUserPayConfigRequest" } } ], @@ -527,13 +466,12 @@ const docTemplate = `{ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { - "type": "integer", - "format": "int64", - "description": "API Key ID", + "type": "string", + "description": "配置ID", "name": "id", "in": "path", "required": true @@ -549,22 +487,37 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/api-keys/{id}/payment-links": { + "/api/v1/admin/users": { "get": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { + "minimum": 1, "type": "integer", - "format": "int64", - "description": "API Key ID", - "name": "id", - "in": "path", - "required": true + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "name": "username", + "in": "query" } ], "responses": { @@ -575,33 +528,31 @@ const docTemplate = `{ } } } - }, - "post": { - "consumes": [ - "application/json" - ], + } + }, + "/api/v1/admin/users/{id}/status": { + "put": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { "type": "integer", - "format": "int64", - "description": "API Key ID", + "description": "用户ID", "name": "id", "in": "path", "required": true }, { - "description": "创建支付链接请求", + "description": "状态", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/link.CreatePaymentLinkRequest" + "$ref": "#/definitions/user.updateUserStatusRequest" } } ], @@ -615,31 +566,16 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/api-keys/{id}/payment-links/{linkId}": { - "delete": { + "/api/v1/config/public": { + "get": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "merchant" - ], - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "API Key ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "format": "int64", - "description": "Payment Link ID", - "name": "linkId", - "in": "path", - "required": true - } + "config" ], "responses": { "200": { @@ -651,8 +587,8 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/payment": { - "post": { + "/api/v1/dashboard/stats/daily": { + "get": { "consumes": [ "application/json" ], @@ -660,17 +596,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "payment" + "dashboard" ], + "summary": "获取每日收支统计", "parameters": [ { - "description": "支付订单请求", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/payment.PayOrderRequest" - } + "type": "integer", + "description": "查询天数,最大7天", + "name": "days", + "in": "query", + "required": true } ], "responses": { @@ -683,8 +618,8 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/payment-links/pay": { - "post": { + "/api/v1/dashboard/stats/top-customers": { + "get": { "consumes": [ "application/json" ], @@ -692,17 +627,23 @@ const docTemplate = `{ "application/json" ], "tags": [ - "merchant" + "dashboard" ], + "summary": "获取Top客户", "parameters": [ { - "description": "支付请求", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/link.PayByLinkRequest" - } + "type": "integer", + "description": "查询天数,最大7天", + "name": "days", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "返回数量,最大10", + "name": "limit", + "in": "query", + "required": true } ], "responses": { @@ -715,21 +656,725 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/payment-links/{token}": { + "/api/v1/dashboard/stats/user-balance": { "get": { + "description": "统计所有用户的AvailableBalance字段", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "merchant" + "dashboard" + ], + "summary": "获取用户余额统计", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dashboard.UserBalanceStatsResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/leaderboard": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leaderboard" + ], + "summary": "获取排行榜列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/leaderboard/me": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leaderboard" + ], + "summary": "获取当前用户排名", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/leaderboard/users/{id}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leaderboard" + ], + "summary": "获取指定用户排名", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_key.CreateAPIKeyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_key.UpdateAPIKeyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys/{id}/payment-links": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "创建支付链接请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/link.PaymentLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys/{id}/payment-links/{linkId}": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Payment Link ID", + "name": "linkId", + "in": "path", + "required": true + }, + { + "description": "更新支付链接请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/link.PaymentLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Payment Link ID", + "name": "linkId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "payment" + ], + "parameters": [ + { + "description": "支付订单请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/payment.PayOrderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment-links/pay": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "description": "支付请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/link.PayByLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment-links/{token}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "string", + "description": "支付链接 Token", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment/order": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "payment" + ], + "parameters": [ + { + "type": "string", + "description": "订单号", + "name": "order_no", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/callback": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oauth.CallbackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/login": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/logout": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/user-info": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/order/dispute": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "order" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dispute.CreateDisputeRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/order/dispute/close": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "order" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dispute.CloseDisputeRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/order/disputes": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "order" ], "parameters": [ { - "type": "string", - "description": "支付链接 Token", - "name": "token", - "in": "path", - "required": true + "description": "request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/dispute.ListDisputesRequest" + } } ], "responses": { @@ -742,8 +1387,8 @@ const docTemplate = `{ } } }, - "/api/v1/merchant/payment/order": { - "get": { + "/api/v1/order/disputes/merchant": { + "post": { "consumes": [ "application/json" ], @@ -751,15 +1396,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "payment" + "order" ], "parameters": [ { - "type": "string", - "description": "订单号", - "name": "order_no", - "in": "query", - "required": true + "description": "request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/dispute.ListDisputesRequest" + } } ], "responses": { @@ -772,13 +1418,16 @@ const docTemplate = `{ } } }, - "/api/v1/oauth/callback": { + "/api/v1/order/refund-review": { "post": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "oauth" + "order" ], "parameters": [ { @@ -787,7 +1436,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oauth.CallbackRequest" + "$ref": "#/definitions/dispute.RefundReviewRequest" } } ], @@ -801,13 +1450,26 @@ const docTemplate = `{ } } }, - "/api/v1/oauth/login": { - "get": { + "/api/v1/order/transactions": { + "post": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "oauth" + "order" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/order.TransactionListRequest" + } + } ], "responses": { "200": { @@ -819,13 +1481,27 @@ const docTemplate = `{ } } }, - "/api/v1/oauth/logout": { - "get": { + "/api/v1/payment/transfer": { + "post": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "oauth" + "payment" + ], + "parameters": [ + { + "description": "转账请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/payment.TransferRequest" + } + } ], "responses": { "200": { @@ -837,13 +1513,13 @@ const docTemplate = `{ } } }, - "/api/v1/oauth/user-info": { + "/api/v1/ready": { "get": { "produces": [ "application/json" ], "tags": [ - "oauth" + "health" ], "responses": { "200": { @@ -851,11 +1527,17 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/util.ResponseAny" } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } } } } }, - "/api/v1/order/dispute": { + "/api/v1/redenvelope/claim": { "post": { "consumes": [ "application/json" @@ -864,16 +1546,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", + "description": "领取红包请求", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dispute.CreateDisputeRequest" + "$ref": "#/definitions/redenvelope.ClaimRequest" } } ], @@ -887,7 +1569,34 @@ const docTemplate = `{ } } }, - "/api/v1/order/dispute/close": { + "/api/v1/redenvelope/covers": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "redenvelope" + ], + "parameters": [ + { + "type": "string", + "description": "封面类型 (cover/heterotypic)", + "name": "type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/redenvelope/create": { "post": { "consumes": [ "application/json" @@ -896,16 +1605,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", + "description": "创建红包请求", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dispute.CloseDisputeRequest" + "$ref": "#/definitions/redenvelope.CreateRequest" } } ], @@ -919,7 +1628,7 @@ const docTemplate = `{ } } }, - "/api/v1/order/disputes": { + "/api/v1/redenvelope/list": { "post": { "consumes": [ "application/json" @@ -928,15 +1637,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", + "description": "列表请求", "name": "request", "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/dispute.ListDisputesRequest" + "$ref": "#/definitions/redenvelope.ListRequest" } } ], @@ -950,25 +1660,21 @@ const docTemplate = `{ } } }, - "/api/v1/order/disputes/merchant": { - "post": { - "consumes": [ - "application/json" - ], + "/api/v1/redenvelope/{id}": { + "get": { "produces": [ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", - "name": "request", - "in": "body", - "schema": { - "$ref": "#/definitions/dispute.ListDisputesRequest" - } + "type": "string", + "description": "红包ID", + "name": "id", + "in": "path", + "required": true } ], "responses": { @@ -981,26 +1687,31 @@ const docTemplate = `{ } } }, - "/api/v1/order/refund-review": { + "/api/v1/upload/redenvelope/cover": { "post": { "consumes": [ - "application/json" + "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ - "order" + "upload" ], "parameters": [ { - "description": "request body", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/dispute.RefundReviewRequest" - } + "type": "file", + "description": "图片文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "封面类型 (cover/heterotypic)", + "name": "type", + "in": "formData", + "required": true } ], "responses": { @@ -1013,8 +1724,8 @@ const docTemplate = `{ } } }, - "/api/v1/order/transactions": { - "post": { + "/api/v1/user/pay-key": { + "put": { "consumes": [ "application/json" ], @@ -1022,15 +1733,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "order" + "user" ], "parameters": [ { "description": "request body", "name": "request", "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/order.TransactionListRequest" + "$ref": "#/definitions/user.UpdatePayKeyRequest" } } ], @@ -1044,7 +1756,31 @@ const docTemplate = `{ } } }, - "/api/v1/payment/transfer": { + "/f/{id}": { + "get": { + "produces": [ + "application/octet-stream" + ], + "tags": [ + "upload" + ], + "parameters": [ + { + "type": "string", + "description": "Upload ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/pay/distribute": { "post": { "consumes": [ "application/json" @@ -1057,12 +1793,19 @@ const docTemplate = `{ ], "parameters": [ { - "description": "转账请求", + "type": "string", + "description": "Basic Auth (base64(client_id:client_secret))", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "分发请求", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/payment.TransferRequest" + "$ref": "#/definitions/payment.MerchantDistributeRequest" } } ], @@ -1076,16 +1819,16 @@ const docTemplate = `{ } } }, - "/api/v1/user/pay-key": { - "put": { + "/pay/submit.php": { + "get": { "consumes": [ - "application/json" + "application/x-www-form-urlencoded" ], "produces": [ "application/json" ], "tags": [ - "user" + "payment" ], "parameters": [ { @@ -1094,7 +1837,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.UpdatePayKeyRequest" + "$ref": "#/definitions/payment.CreateOrderRequest" } } ], @@ -1106,9 +1849,7 @@ const docTemplate = `{ } } } - } - }, - "/pay/submit.php": { + }, "post": { "consumes": [ "application/x-www-form-urlencoded" @@ -1166,9 +1907,16 @@ const docTemplate = `{ "type": "string", "maxLength": 100 }, + "public_key": { + "type": "string", + "maxLength": 100 + }, "redirect_uri": { "type": "string", "maxLength": 100 + }, + "test_mode": { + "type": "boolean" } } }, @@ -1179,21 +1927,54 @@ const docTemplate = `{ "type": "string", "maxLength": 100 }, - "app_homepage_url": { - "type": "string", - "maxLength": 100 + "app_homepage_url": { + "type": "string", + "maxLength": 100 + }, + "app_name": { + "type": "string", + "maxLength": 20 + }, + "notify_url": { + "type": "string", + "maxLength": 100 + }, + "public_key": { + "type": "string", + "maxLength": 100 + }, + "redirect_uri": { + "type": "string", + "maxLength": 100 + }, + "test_mode": { + "type": "boolean" + } + } + }, + "dashboard.UserBalanceStatsResponse": { + "type": "object", + "properties": { + "avg_amount": { + "type": "number" + }, + "max_amount": { + "type": "number" + }, + "median_amount": { + "type": "number" }, - "app_name": { - "type": "string", - "maxLength": 20 + "min_amount": { + "type": "number" }, - "notify_url": { - "type": "string", - "maxLength": 100 + "std_dev": { + "type": "number" }, - "redirect_uri": { - "type": "string", - "maxLength": 100 + "total_amount": { + "type": "number" + }, + "total_count": { + "type": "integer" } } }, @@ -1204,7 +1985,8 @@ const docTemplate = `{ ], "properties": { "dispute_id": { - "type": "integer" + "type": "string", + "example": "0" } } }, @@ -1216,7 +1998,8 @@ const docTemplate = `{ ], "properties": { "order_id": { - "type": "integer" + "type": "string", + "example": "0" }, "reason": { "type": "string", @@ -1228,7 +2011,8 @@ const docTemplate = `{ "type": "object", "properties": { "dispute_id": { - "type": "integer" + "type": "string", + "example": "0" }, "page": { "type": "integer", @@ -1257,7 +2041,8 @@ const docTemplate = `{ ], "properties": { "dispute_id": { - "type": "integer" + "type": "string", + "example": "0" }, "reason": { "type": "string", @@ -1272,46 +2057,65 @@ const docTemplate = `{ } } }, - "link.CreatePaymentLinkRequest": { + "link.PayByLinkRequest": { "type": "object", "required": [ - "amount", - "product_name" + "pay_key", + "token" ], "properties": { - "amount": { - "type": "number" - }, - "product_name": { + "pay_key": { "type": "string", - "maxLength": 30 + "maxLength": 6 }, "remark": { "type": "string", "maxLength": 100 + }, + "token": { + "type": "string" } } }, - "link.PayByLinkRequest": { + "link.PaymentLinkRequest": { "type": "object", "required": [ - "pay_key", - "token" + "amount", + "product_name" ], "properties": { - "pay_key": { + "amount": { + "type": "number" + }, + "product_name": { "type": "string", - "maxLength": 6 + "maxLength": 30 }, "remark": { "type": "string", "maxLength": 100 }, - "token": { - "type": "string" + "total_limit": { + "type": "integer", + "minimum": 1 + }, + "user_limit": { + "type": "integer", + "minimum": 1 } } }, + "model.OrderTransferStatus": { + "type": "string", + "enum": [ + "pending", + "completed" + ], + "x-enum-varnames": [ + "OrderTransferStatusPending", + "OrderTransferStatusCompleted" + ] + }, "model.PayLevel": { "type": "integer", "format": "int32", @@ -1328,6 +2132,17 @@ const docTemplate = `{ "PayLevelPremium" ] }, + "model.RedEnvelopeType": { + "type": "string", + "enum": [ + "fixed", + "random" + ], + "x-enum-varnames": [ + "RedEnvelopeTypeFixed", + "RedEnvelopeTypeRandom" + ] + }, "oauth.CallbackRequest": { "type": "object", "properties": { @@ -1348,6 +2163,13 @@ const docTemplate = `{ "endTime": { "type": "string" }, + "id": { + "type": "string", + "example": "0" + }, + "order_name": { + "type": "string" + }, "page": { "type": "integer", "minimum": 1 @@ -1357,30 +2179,102 @@ const docTemplate = `{ "maximum": 100, "minimum": 1 }, + "payee_transfer_status": { + "enum": [ + "pending", + "completed" + ], + "allOf": [ + { + "$ref": "#/definitions/model.OrderTransferStatus" + } + ] + }, + "payee_username": { + "type": "string" + }, + "payer_username": { + "type": "string" + }, "startTime": { "type": "string" }, - "status": { + "statuses": { + "type": "array", + "items": { + "type": "string" + } + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "order.listOrdersRequest": { + "type": "object", + "properties": { + "client_id": { "type": "string", - "enum": [ - "success", - "pending", - "failed", - "expired", - "disputing", - "refund", - "refused" - ] + "maxLength": 64 }, - "type": { + "end_time": { + "type": "string" + }, + "id": { "type": "string", - "enum": [ - "receive", - "payment", - "transfer", - "community", - "online" - ] + "example": "0" + }, + "merchant_order_no": { + "type": "string", + "maxLength": 64 + }, + "order_name": { + "type": "string", + "maxLength": 64 + }, + "page": { + "type": "integer", + "minimum": 1 + }, + "page_size": { + "type": "integer", + "maximum": 100, + "minimum": 1 + }, + "payee_username": { + "type": "string", + "maxLength": 255 + }, + "payer_username": { + "type": "string", + "maxLength": 255 + }, + "start_time": { + "type": "string" + }, + "statuses": { + "type": "array", + "items": { + "type": "string" + } + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "order.refundOrderRequest": { + "type": "object", + "properties": { + "remark": { + "type": "string", + "maxLength": 100 } } }, @@ -1395,7 +2289,13 @@ const docTemplate = `{ "type": "number" }, "merchant_order_no": { - "type": "string" + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "notify_url": { + "type": "string", + "maxLength": 100 }, "order_name": { "type": "string", @@ -1407,6 +2307,38 @@ const docTemplate = `{ "remark": { "type": "string", "maxLength": 100 + }, + "return_url": { + "type": "string", + "maxLength": 100 + } + } + }, + "payment.MerchantDistributeRequest": { + "type": "object", + "required": [ + "amount", + "user_id", + "username" + ], + "properties": { + "amount": { + "type": "number" + }, + "out_trade_no": { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "remark": { + "type": "string", + "maxLength": 100 + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" } } }, @@ -1531,7 +2463,8 @@ const docTemplate = `{ "maxLength": 6 }, "recipient_id": { - "type": "integer" + "type": "string", + "example": "0" }, "recipient_username": { "type": "string" @@ -1542,6 +2475,88 @@ const docTemplate = `{ } } }, + "redenvelope.ClaimRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "example": "0" + } + } + }, + "redenvelope.CreateRequest": { + "type": "object", + "required": [ + "pay_key", + "total_amount", + "total_count", + "type" + ], + "properties": { + "cover_upload_id": { + "type": "string", + "example": "0" + }, + "greeting": { + "type": "string", + "maxLength": 100 + }, + "heterotypic_upload_id": { + "type": "string", + "example": "0" + }, + "pay_key": { + "type": "string", + "maxLength": 10 + }, + "total_amount": { + "type": "number" + }, + "total_count": { + "type": "integer", + "minimum": 1 + }, + "type": { + "enum": [ + "fixed", + "random" + ], + "allOf": [ + { + "$ref": "#/definitions/model.RedEnvelopeType" + } + ] + } + } + }, + "redenvelope.ListRequest": { + "type": "object", + "required": [ + "page", + "page_size" + ], + "properties": { + "page": { + "type": "integer", + "minimum": 1 + }, + "page_size": { + "type": "integer", + "maximum": 100, + "minimum": 1 + }, + "type": { + "type": "string", + "enum": [ + "sent", + "received" + ] + } + } + }, "system_config.CreateSystemConfigRequest": { "type": "object", "required": [ @@ -1579,6 +2594,26 @@ const docTemplate = `{ } } }, + "task.DispatchTaskRequest": { + "type": "object", + "required": [ + "task_type" + ], + "properties": { + "end_time": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "task_type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "user.UpdatePayKeyRequest": { "type": "object", "required": [ @@ -1591,9 +2626,18 @@ const docTemplate = `{ } } }, + "user.updateUserStatusRequest": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + } + } + }, "user_pay_config.CreateUserPayConfigRequest": { "type": "object", "required": [ + "distribute_rate", "fee_rate", "score_rate" ], @@ -1601,6 +2645,9 @@ const docTemplate = `{ "daily_limit": { "type": "integer" }, + "distribute_rate": { + "type": "number" + }, "fee_rate": { "type": "number" }, @@ -1611,8 +2658,7 @@ const docTemplate = `{ "type": "integer" }, "min_score": { - "type": "integer", - "minimum": 0 + "type": "integer" }, "score_rate": { "type": "number" @@ -1622,6 +2668,7 @@ const docTemplate = `{ "user_pay_config.UpdateUserPayConfigRequest": { "type": "object", "required": [ + "distribute_rate", "fee_rate", "score_rate" ], @@ -1629,6 +2676,9 @@ const docTemplate = `{ "daily_limit": { "type": "integer" }, + "distribute_rate": { + "type": "number" + }, "fee_rate": { "type": "number" }, @@ -1636,8 +2686,7 @@ const docTemplate = `{ "type": "integer" }, "min_score": { - "type": "integer", - "minimum": 0 + "type": "integer" }, "score_rate": { "type": "number" @@ -1659,7 +2708,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "0.1.0", + Version: "1.0.0", Host: "", BasePath: "", Schemes: []string{}, diff --git a/docs/swagger.json b/docs/swagger.json index 6d6c93c0..003dc0fb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3,7 +3,7 @@ "info": { "title": "LINUX DO Credit", "contact": {}, - "version": "0.1.0" + "version": "1.0.0" }, "paths": { "/api.php": { @@ -30,19 +30,16 @@ "required": true }, { + "maxLength": 64, + "minLength": 1, "type": "string", "name": "out_trade_no", - "in": "query" - }, - { - "type": "string", - "name": "pid", "in": "query", "required": true }, { - "type": "integer", - "name": "trade_no", + "type": "string", + "name": "pid", "in": "query", "required": true } @@ -87,23 +84,7 @@ } } }, - "/api/v1/admin/system-configs": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/util.ResponseAny" - } - } - } - }, + "/api/v1/admin/orders": { "post": { "consumes": [ "application/json" @@ -121,7 +102,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/system_config.CreateSystemConfigRequest" + "$ref": "#/definitions/order.listOrdersRequest" } } ], @@ -135,33 +116,8 @@ } } }, - "/api/v1/admin/system-configs/{key}": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "parameters": [ - { - "type": "string", - "description": "配置键", - "name": "key", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/util.ResponseAny" - } - } - } - }, - "put": { + "/api/v1/admin/orders/{id}/refund": { + "post": { "consumes": [ "application/json" ], @@ -173,9 +129,9 @@ ], "parameters": [ { - "type": "string", - "description": "配置键", - "name": "key", + "type": "integer", + "description": "订单ID", + "name": "id", "in": "path", "required": true }, @@ -183,36 +139,10 @@ "description": "request body", "name": "request", "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/system_config.UpdateSystemConfigRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", "schema": { - "$ref": "#/definitions/util.ResponseAny" + "$ref": "#/definitions/order.refundOrderRequest" } } - } - }, - "delete": { - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "parameters": [ - { - "type": "string", - "description": "配置键", - "name": "key", - "in": "path", - "required": true - } ], "responses": { "200": { @@ -224,7 +154,7 @@ } } }, - "/api/v1/admin/user-pay-configs": { + "/api/v1/admin/system-configs": { "get": { "produces": [ "application/json" @@ -258,7 +188,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user_pay_config.CreateUserPayConfigRequest" + "$ref": "#/definitions/system_config.CreateSystemConfigRequest" } } ], @@ -272,7 +202,7 @@ } } }, - "/api/v1/admin/user-pay-configs/{id}": { + "/api/v1/admin/system-configs/{key}": { "get": { "produces": [ "application/json" @@ -283,8 +213,8 @@ "parameters": [ { "type": "string", - "description": "配置ID", - "name": "id", + "description": "配置键", + "name": "key", "in": "path", "required": true } @@ -311,8 +241,8 @@ "parameters": [ { "type": "string", - "description": "配置ID", - "name": "id", + "description": "配置键", + "name": "key", "in": "path", "required": true }, @@ -322,7 +252,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user_pay_config.UpdateUserPayConfigRequest" + "$ref": "#/definitions/system_config.UpdateSystemConfigRequest" } } ], @@ -345,8 +275,8 @@ "parameters": [ { "type": "string", - "description": "配置ID", - "name": "id", + "description": "配置键", + "name": "key", "in": "path", "required": true } @@ -361,8 +291,8 @@ } } }, - "/api/v1/config/public": { - "get": { + "/api/v1/admin/tasks/dispatch": { + "post": { "consumes": [ "application/json" ], @@ -370,7 +300,18 @@ "application/json" ], "tags": [ - "config" + "admin" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/task.DispatchTaskRequest" + } + } ], "responses": { "200": { @@ -382,13 +323,13 @@ } } }, - "/api/v1/health": { + "/api/v1/admin/tasks/types": { "get": { "produces": [ "application/json" ], "tags": [ - "health" + "admin" ], "responses": { "200": { @@ -400,13 +341,13 @@ } } }, - "/api/v1/merchant/api-keys": { + "/api/v1/admin/user-pay-configs": { "get": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "responses": { "200": { @@ -425,7 +366,7 @@ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { @@ -434,7 +375,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/api_key.CreateAPIKeyRequest" + "$ref": "#/definitions/user_pay_config.CreateUserPayConfigRequest" } } ], @@ -448,19 +389,18 @@ } } }, - "/api/v1/merchant/api-keys/{id}": { + "/api/v1/admin/user-pay-configs/{id}": { "get": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { - "type": "integer", - "format": "int64", - "description": "API Key ID", + "type": "string", + "description": "配置ID", "name": "id", "in": "path", "required": true @@ -483,13 +423,12 @@ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { - "type": "integer", - "format": "int64", - "description": "API Key ID", + "type": "string", + "description": "配置ID", "name": "id", "in": "path", "required": true @@ -500,7 +439,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/api_key.UpdateAPIKeyRequest" + "$ref": "#/definitions/user_pay_config.UpdateUserPayConfigRequest" } } ], @@ -518,13 +457,12 @@ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { - "type": "integer", - "format": "int64", - "description": "API Key ID", + "type": "string", + "description": "配置ID", "name": "id", "in": "path", "required": true @@ -540,22 +478,37 @@ } } }, - "/api/v1/merchant/api-keys/{id}/payment-links": { + "/api/v1/admin/users": { "get": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { + "minimum": 1, "type": "integer", - "format": "int64", - "description": "API Key ID", - "name": "id", - "in": "path", - "required": true + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "name": "username", + "in": "query" } ], "responses": { @@ -566,33 +519,31 @@ } } } - }, - "post": { - "consumes": [ - "application/json" - ], + } + }, + "/api/v1/admin/users/{id}/status": { + "put": { "produces": [ "application/json" ], "tags": [ - "merchant" + "admin" ], "parameters": [ { "type": "integer", - "format": "int64", - "description": "API Key ID", + "description": "用户ID", "name": "id", "in": "path", "required": true }, { - "description": "创建支付链接请求", + "description": "状态", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/link.CreatePaymentLinkRequest" + "$ref": "#/definitions/user.updateUserStatusRequest" } } ], @@ -606,31 +557,16 @@ } } }, - "/api/v1/merchant/api-keys/{id}/payment-links/{linkId}": { - "delete": { + "/api/v1/config/public": { + "get": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "merchant" - ], - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "API Key ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "format": "int64", - "description": "Payment Link ID", - "name": "linkId", - "in": "path", - "required": true - } + "config" ], "responses": { "200": { @@ -642,8 +578,8 @@ } } }, - "/api/v1/merchant/payment": { - "post": { + "/api/v1/dashboard/stats/daily": { + "get": { "consumes": [ "application/json" ], @@ -651,17 +587,16 @@ "application/json" ], "tags": [ - "payment" + "dashboard" ], + "summary": "获取每日收支统计", "parameters": [ { - "description": "支付订单请求", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/payment.PayOrderRequest" - } + "type": "integer", + "description": "查询天数,最大7天", + "name": "days", + "in": "query", + "required": true } ], "responses": { @@ -674,8 +609,8 @@ } } }, - "/api/v1/merchant/payment-links/pay": { - "post": { + "/api/v1/dashboard/stats/top-customers": { + "get": { "consumes": [ "application/json" ], @@ -683,17 +618,23 @@ "application/json" ], "tags": [ - "merchant" + "dashboard" ], + "summary": "获取Top客户", "parameters": [ { - "description": "支付请求", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/link.PayByLinkRequest" - } + "type": "integer", + "description": "查询天数,最大7天", + "name": "days", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "返回数量,最大10", + "name": "limit", + "in": "query", + "required": true } ], "responses": { @@ -706,21 +647,725 @@ } } }, - "/api/v1/merchant/payment-links/{token}": { + "/api/v1/dashboard/stats/user-balance": { "get": { + "description": "统计所有用户的AvailableBalance字段", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "merchant" + "dashboard" + ], + "summary": "获取用户余额统计", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/util.ResponseAny" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dashboard.UserBalanceStatsResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/leaderboard": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leaderboard" + ], + "summary": "获取排行榜列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/leaderboard/me": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leaderboard" + ], + "summary": "获取当前用户排名", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/leaderboard/users/{id}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leaderboard" + ], + "summary": "获取指定用户排名", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_key.CreateAPIKeyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_key.UpdateAPIKeyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys/{id}/payment-links": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "创建支付链接请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/link.PaymentLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/api-keys/{id}/payment-links/{linkId}": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Payment Link ID", + "name": "linkId", + "in": "path", + "required": true + }, + { + "description": "更新支付链接请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/link.PaymentLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "API Key ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Payment Link ID", + "name": "linkId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "payment" + ], + "parameters": [ + { + "description": "支付订单请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/payment.PayOrderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment-links/pay": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "description": "支付请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/link.PayByLinkRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment-links/{token}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "merchant" + ], + "parameters": [ + { + "type": "string", + "description": "支付链接 Token", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/merchant/payment/order": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "payment" + ], + "parameters": [ + { + "type": "string", + "description": "订单号", + "name": "order_no", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/callback": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/oauth.CallbackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/login": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/logout": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/oauth/user-info": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/order/dispute": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "order" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dispute.CreateDisputeRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/order/dispute/close": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "order" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dispute.CloseDisputeRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/order/disputes": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "order" ], "parameters": [ { - "type": "string", - "description": "支付链接 Token", - "name": "token", - "in": "path", - "required": true + "description": "request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/dispute.ListDisputesRequest" + } } ], "responses": { @@ -733,8 +1378,8 @@ } } }, - "/api/v1/merchant/payment/order": { - "get": { + "/api/v1/order/disputes/merchant": { + "post": { "consumes": [ "application/json" ], @@ -742,15 +1387,16 @@ "application/json" ], "tags": [ - "payment" + "order" ], "parameters": [ { - "type": "string", - "description": "订单号", - "name": "order_no", - "in": "query", - "required": true + "description": "request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/dispute.ListDisputesRequest" + } } ], "responses": { @@ -763,13 +1409,16 @@ } } }, - "/api/v1/oauth/callback": { + "/api/v1/order/refund-review": { "post": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "oauth" + "order" ], "parameters": [ { @@ -778,7 +1427,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/oauth.CallbackRequest" + "$ref": "#/definitions/dispute.RefundReviewRequest" } } ], @@ -792,13 +1441,26 @@ } } }, - "/api/v1/oauth/login": { - "get": { + "/api/v1/order/transactions": { + "post": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "oauth" + "order" + ], + "parameters": [ + { + "description": "request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/order.TransactionListRequest" + } + } ], "responses": { "200": { @@ -810,13 +1472,27 @@ } } }, - "/api/v1/oauth/logout": { - "get": { + "/api/v1/payment/transfer": { + "post": { + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "oauth" + "payment" + ], + "parameters": [ + { + "description": "转账请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/payment.TransferRequest" + } + } ], "responses": { "200": { @@ -828,13 +1504,13 @@ } } }, - "/api/v1/oauth/user-info": { + "/api/v1/ready": { "get": { "produces": [ "application/json" ], "tags": [ - "oauth" + "health" ], "responses": { "200": { @@ -842,11 +1518,17 @@ "schema": { "$ref": "#/definitions/util.ResponseAny" } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } } } } }, - "/api/v1/order/dispute": { + "/api/v1/redenvelope/claim": { "post": { "consumes": [ "application/json" @@ -855,16 +1537,16 @@ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", + "description": "领取红包请求", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dispute.CreateDisputeRequest" + "$ref": "#/definitions/redenvelope.ClaimRequest" } } ], @@ -878,7 +1560,34 @@ } } }, - "/api/v1/order/dispute/close": { + "/api/v1/redenvelope/covers": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "redenvelope" + ], + "parameters": [ + { + "type": "string", + "description": "封面类型 (cover/heterotypic)", + "name": "type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, + "/api/v1/redenvelope/create": { "post": { "consumes": [ "application/json" @@ -887,16 +1596,16 @@ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", + "description": "创建红包请求", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dispute.CloseDisputeRequest" + "$ref": "#/definitions/redenvelope.CreateRequest" } } ], @@ -910,7 +1619,7 @@ } } }, - "/api/v1/order/disputes": { + "/api/v1/redenvelope/list": { "post": { "consumes": [ "application/json" @@ -919,15 +1628,16 @@ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", + "description": "列表请求", "name": "request", "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/dispute.ListDisputesRequest" + "$ref": "#/definitions/redenvelope.ListRequest" } } ], @@ -941,25 +1651,21 @@ } } }, - "/api/v1/order/disputes/merchant": { - "post": { - "consumes": [ - "application/json" - ], + "/api/v1/redenvelope/{id}": { + "get": { "produces": [ "application/json" ], "tags": [ - "order" + "redenvelope" ], "parameters": [ { - "description": "request body", - "name": "request", - "in": "body", - "schema": { - "$ref": "#/definitions/dispute.ListDisputesRequest" - } + "type": "string", + "description": "红包ID", + "name": "id", + "in": "path", + "required": true } ], "responses": { @@ -972,26 +1678,31 @@ } } }, - "/api/v1/order/refund-review": { + "/api/v1/upload/redenvelope/cover": { "post": { "consumes": [ - "application/json" + "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ - "order" + "upload" ], "parameters": [ { - "description": "request body", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/dispute.RefundReviewRequest" - } + "type": "file", + "description": "图片文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "封面类型 (cover/heterotypic)", + "name": "type", + "in": "formData", + "required": true } ], "responses": { @@ -1004,8 +1715,8 @@ } } }, - "/api/v1/order/transactions": { - "post": { + "/api/v1/user/pay-key": { + "put": { "consumes": [ "application/json" ], @@ -1013,15 +1724,16 @@ "application/json" ], "tags": [ - "order" + "user" ], "parameters": [ { "description": "request body", "name": "request", "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/order.TransactionListRequest" + "$ref": "#/definitions/user.UpdatePayKeyRequest" } } ], @@ -1035,7 +1747,31 @@ } } }, - "/api/v1/payment/transfer": { + "/f/{id}": { + "get": { + "produces": [ + "application/octet-stream" + ], + "tags": [ + "upload" + ], + "parameters": [ + { + "type": "string", + "description": "Upload ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/pay/distribute": { "post": { "consumes": [ "application/json" @@ -1048,12 +1784,19 @@ ], "parameters": [ { - "description": "转账请求", + "type": "string", + "description": "Basic Auth (base64(client_id:client_secret))", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "分发请求", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/payment.TransferRequest" + "$ref": "#/definitions/payment.MerchantDistributeRequest" } } ], @@ -1067,16 +1810,16 @@ } } }, - "/api/v1/user/pay-key": { - "put": { + "/pay/submit.php": { + "get": { "consumes": [ - "application/json" + "application/x-www-form-urlencoded" ], "produces": [ "application/json" ], "tags": [ - "user" + "payment" ], "parameters": [ { @@ -1085,7 +1828,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.UpdatePayKeyRequest" + "$ref": "#/definitions/payment.CreateOrderRequest" } } ], @@ -1097,9 +1840,7 @@ } } } - } - }, - "/pay/submit.php": { + }, "post": { "consumes": [ "application/x-www-form-urlencoded" @@ -1157,9 +1898,16 @@ "type": "string", "maxLength": 100 }, + "public_key": { + "type": "string", + "maxLength": 100 + }, "redirect_uri": { "type": "string", "maxLength": 100 + }, + "test_mode": { + "type": "boolean" } } }, @@ -1170,21 +1918,54 @@ "type": "string", "maxLength": 100 }, - "app_homepage_url": { - "type": "string", - "maxLength": 100 + "app_homepage_url": { + "type": "string", + "maxLength": 100 + }, + "app_name": { + "type": "string", + "maxLength": 20 + }, + "notify_url": { + "type": "string", + "maxLength": 100 + }, + "public_key": { + "type": "string", + "maxLength": 100 + }, + "redirect_uri": { + "type": "string", + "maxLength": 100 + }, + "test_mode": { + "type": "boolean" + } + } + }, + "dashboard.UserBalanceStatsResponse": { + "type": "object", + "properties": { + "avg_amount": { + "type": "number" + }, + "max_amount": { + "type": "number" + }, + "median_amount": { + "type": "number" }, - "app_name": { - "type": "string", - "maxLength": 20 + "min_amount": { + "type": "number" }, - "notify_url": { - "type": "string", - "maxLength": 100 + "std_dev": { + "type": "number" }, - "redirect_uri": { - "type": "string", - "maxLength": 100 + "total_amount": { + "type": "number" + }, + "total_count": { + "type": "integer" } } }, @@ -1195,7 +1976,8 @@ ], "properties": { "dispute_id": { - "type": "integer" + "type": "string", + "example": "0" } } }, @@ -1207,7 +1989,8 @@ ], "properties": { "order_id": { - "type": "integer" + "type": "string", + "example": "0" }, "reason": { "type": "string", @@ -1219,7 +2002,8 @@ "type": "object", "properties": { "dispute_id": { - "type": "integer" + "type": "string", + "example": "0" }, "page": { "type": "integer", @@ -1248,7 +2032,8 @@ ], "properties": { "dispute_id": { - "type": "integer" + "type": "string", + "example": "0" }, "reason": { "type": "string", @@ -1263,46 +2048,65 @@ } } }, - "link.CreatePaymentLinkRequest": { + "link.PayByLinkRequest": { "type": "object", "required": [ - "amount", - "product_name" + "pay_key", + "token" ], "properties": { - "amount": { - "type": "number" - }, - "product_name": { + "pay_key": { "type": "string", - "maxLength": 30 + "maxLength": 6 }, "remark": { "type": "string", "maxLength": 100 + }, + "token": { + "type": "string" } } }, - "link.PayByLinkRequest": { + "link.PaymentLinkRequest": { "type": "object", "required": [ - "pay_key", - "token" + "amount", + "product_name" ], "properties": { - "pay_key": { + "amount": { + "type": "number" + }, + "product_name": { "type": "string", - "maxLength": 6 + "maxLength": 30 }, "remark": { "type": "string", "maxLength": 100 }, - "token": { - "type": "string" + "total_limit": { + "type": "integer", + "minimum": 1 + }, + "user_limit": { + "type": "integer", + "minimum": 1 } } }, + "model.OrderTransferStatus": { + "type": "string", + "enum": [ + "pending", + "completed" + ], + "x-enum-varnames": [ + "OrderTransferStatusPending", + "OrderTransferStatusCompleted" + ] + }, "model.PayLevel": { "type": "integer", "format": "int32", @@ -1319,6 +2123,17 @@ "PayLevelPremium" ] }, + "model.RedEnvelopeType": { + "type": "string", + "enum": [ + "fixed", + "random" + ], + "x-enum-varnames": [ + "RedEnvelopeTypeFixed", + "RedEnvelopeTypeRandom" + ] + }, "oauth.CallbackRequest": { "type": "object", "properties": { @@ -1339,6 +2154,13 @@ "endTime": { "type": "string" }, + "id": { + "type": "string", + "example": "0" + }, + "order_name": { + "type": "string" + }, "page": { "type": "integer", "minimum": 1 @@ -1348,30 +2170,102 @@ "maximum": 100, "minimum": 1 }, + "payee_transfer_status": { + "enum": [ + "pending", + "completed" + ], + "allOf": [ + { + "$ref": "#/definitions/model.OrderTransferStatus" + } + ] + }, + "payee_username": { + "type": "string" + }, + "payer_username": { + "type": "string" + }, "startTime": { "type": "string" }, - "status": { + "statuses": { + "type": "array", + "items": { + "type": "string" + } + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "order.listOrdersRequest": { + "type": "object", + "properties": { + "client_id": { "type": "string", - "enum": [ - "success", - "pending", - "failed", - "expired", - "disputing", - "refund", - "refused" - ] + "maxLength": 64 }, - "type": { + "end_time": { + "type": "string" + }, + "id": { "type": "string", - "enum": [ - "receive", - "payment", - "transfer", - "community", - "online" - ] + "example": "0" + }, + "merchant_order_no": { + "type": "string", + "maxLength": 64 + }, + "order_name": { + "type": "string", + "maxLength": 64 + }, + "page": { + "type": "integer", + "minimum": 1 + }, + "page_size": { + "type": "integer", + "maximum": 100, + "minimum": 1 + }, + "payee_username": { + "type": "string", + "maxLength": 255 + }, + "payer_username": { + "type": "string", + "maxLength": 255 + }, + "start_time": { + "type": "string" + }, + "statuses": { + "type": "array", + "items": { + "type": "string" + } + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "order.refundOrderRequest": { + "type": "object", + "properties": { + "remark": { + "type": "string", + "maxLength": 100 } } }, @@ -1386,7 +2280,13 @@ "type": "number" }, "merchant_order_no": { - "type": "string" + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "notify_url": { + "type": "string", + "maxLength": 100 }, "order_name": { "type": "string", @@ -1398,6 +2298,38 @@ "remark": { "type": "string", "maxLength": 100 + }, + "return_url": { + "type": "string", + "maxLength": 100 + } + } + }, + "payment.MerchantDistributeRequest": { + "type": "object", + "required": [ + "amount", + "user_id", + "username" + ], + "properties": { + "amount": { + "type": "number" + }, + "out_trade_no": { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "remark": { + "type": "string", + "maxLength": 100 + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" } } }, @@ -1522,7 +2454,8 @@ "maxLength": 6 }, "recipient_id": { - "type": "integer" + "type": "string", + "example": "0" }, "recipient_username": { "type": "string" @@ -1533,6 +2466,88 @@ } } }, + "redenvelope.ClaimRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "example": "0" + } + } + }, + "redenvelope.CreateRequest": { + "type": "object", + "required": [ + "pay_key", + "total_amount", + "total_count", + "type" + ], + "properties": { + "cover_upload_id": { + "type": "string", + "example": "0" + }, + "greeting": { + "type": "string", + "maxLength": 100 + }, + "heterotypic_upload_id": { + "type": "string", + "example": "0" + }, + "pay_key": { + "type": "string", + "maxLength": 10 + }, + "total_amount": { + "type": "number" + }, + "total_count": { + "type": "integer", + "minimum": 1 + }, + "type": { + "enum": [ + "fixed", + "random" + ], + "allOf": [ + { + "$ref": "#/definitions/model.RedEnvelopeType" + } + ] + } + } + }, + "redenvelope.ListRequest": { + "type": "object", + "required": [ + "page", + "page_size" + ], + "properties": { + "page": { + "type": "integer", + "minimum": 1 + }, + "page_size": { + "type": "integer", + "maximum": 100, + "minimum": 1 + }, + "type": { + "type": "string", + "enum": [ + "sent", + "received" + ] + } + } + }, "system_config.CreateSystemConfigRequest": { "type": "object", "required": [ @@ -1570,6 +2585,26 @@ } } }, + "task.DispatchTaskRequest": { + "type": "object", + "required": [ + "task_type" + ], + "properties": { + "end_time": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "task_type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "user.UpdatePayKeyRequest": { "type": "object", "required": [ @@ -1582,9 +2617,18 @@ } } }, + "user.updateUserStatusRequest": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + } + } + }, "user_pay_config.CreateUserPayConfigRequest": { "type": "object", "required": [ + "distribute_rate", "fee_rate", "score_rate" ], @@ -1592,6 +2636,9 @@ "daily_limit": { "type": "integer" }, + "distribute_rate": { + "type": "number" + }, "fee_rate": { "type": "number" }, @@ -1602,8 +2649,7 @@ "type": "integer" }, "min_score": { - "type": "integer", - "minimum": 0 + "type": "integer" }, "score_rate": { "type": "number" @@ -1613,6 +2659,7 @@ "user_pay_config.UpdateUserPayConfigRequest": { "type": "object", "required": [ + "distribute_rate", "fee_rate", "score_rate" ], @@ -1620,6 +2667,9 @@ "daily_limit": { "type": "integer" }, + "distribute_rate": { + "type": "number" + }, "fee_rate": { "type": "number" }, @@ -1627,8 +2677,7 @@ "type": "integer" }, "min_score": { - "type": "integer", - "minimum": 0 + "type": "integer" }, "score_rate": { "type": "number" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index dd7284bf..f42a3d17 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -13,9 +13,14 @@ definitions: notify_url: maxLength: 100 type: string + public_key: + maxLength: 100 + type: string redirect_uri: maxLength: 100 type: string + test_mode: + type: boolean required: - app_homepage_url - app_name @@ -35,21 +40,45 @@ definitions: notify_url: maxLength: 100 type: string + public_key: + maxLength: 100 + type: string redirect_uri: maxLength: 100 type: string + test_mode: + type: boolean + type: object + dashboard.UserBalanceStatsResponse: + properties: + avg_amount: + type: number + max_amount: + type: number + median_amount: + type: number + min_amount: + type: number + std_dev: + type: number + total_amount: + type: number + total_count: + type: integer type: object dispute.CloseDisputeRequest: properties: dispute_id: - type: integer + example: "0" + type: string required: - dispute_id type: object dispute.CreateDisputeRequest: properties: order_id: - type: integer + example: "0" + type: string reason: maxLength: 100 type: string @@ -60,7 +89,8 @@ definitions: dispute.ListDisputesRequest: properties: dispute_id: - type: integer + example: "0" + type: string page: minimum: 1 type: integer @@ -78,7 +108,8 @@ definitions: dispute.RefundReviewRequest: properties: dispute_id: - type: integer + example: "0" + type: string reason: maxLength: 100 type: string @@ -91,20 +122,6 @@ definitions: - dispute_id - status type: object - link.CreatePaymentLinkRequest: - properties: - amount: - type: number - product_name: - maxLength: 30 - type: string - remark: - maxLength: 100 - type: string - required: - - amount - - product_name - type: object link.PayByLinkRequest: properties: pay_key: @@ -119,6 +136,34 @@ definitions: - pay_key - token type: object + link.PaymentLinkRequest: + properties: + amount: + type: number + product_name: + maxLength: 30 + type: string + remark: + maxLength: 100 + type: string + total_limit: + minimum: 1 + type: integer + user_limit: + minimum: 1 + type: integer + required: + - amount + - product_name + type: object + model.OrderTransferStatus: + enum: + - pending + - completed + type: string + x-enum-varnames: + - OrderTransferStatusPending + - OrderTransferStatusCompleted model.PayLevel: enum: - 0 @@ -132,6 +177,14 @@ definitions: - PayLevelBasic - PayLevelStandard - PayLevelPremium + model.RedEnvelopeType: + enum: + - fixed + - random + type: string + x-enum-varnames: + - RedEnvelopeTypeFixed + - RedEnvelopeTypeRandom oauth.CallbackRequest: properties: code: @@ -145,6 +198,11 @@ definitions: type: string endTime: type: string + id: + example: "0" + type: string + order_name: + type: string page: minimum: 1 type: integer @@ -152,25 +210,71 @@ definitions: maximum: 100 minimum: 1 type: integer - startTime: - type: string - status: + payee_transfer_status: + allOf: + - $ref: '#/definitions/model.OrderTransferStatus' enum: - - success - pending - - failed - - expired - - disputing - - refund - - refused + - completed + payee_username: type: string - type: - enum: - - receive - - payment - - transfer - - community - - online + payer_username: + type: string + startTime: + type: string + statuses: + items: + type: string + type: array + types: + items: + type: string + type: array + type: object + order.listOrdersRequest: + properties: + client_id: + maxLength: 64 + type: string + end_time: + type: string + id: + example: "0" + type: string + merchant_order_no: + maxLength: 64 + type: string + order_name: + maxLength: 64 + type: string + page: + minimum: 1 + type: integer + page_size: + maximum: 100 + minimum: 1 + type: integer + payee_username: + maxLength: 255 + type: string + payer_username: + maxLength: 255 + type: string + start_time: + type: string + statuses: + items: + type: string + type: array + types: + items: + type: string + type: array + type: object + order.refundOrderRequest: + properties: + remark: + maxLength: 100 type: string type: object payment.CreateOrderRequest: @@ -178,6 +282,11 @@ definitions: amount: type: number merchant_order_no: + maxLength: 64 + minLength: 1 + type: string + notify_url: + maxLength: 100 type: string order_name: maxLength: 64 @@ -187,10 +296,33 @@ definitions: remark: maxLength: 100 type: string + return_url: + maxLength: 100 + type: string required: - amount - order_name type: object + payment.MerchantDistributeRequest: + properties: + amount: + type: number + out_trade_no: + maxLength: 64 + minLength: 1 + type: string + remark: + maxLength: 100 + type: string + user_id: + type: integer + username: + type: string + required: + - amount + - user_id + - username + type: object payment.PayOrderRequest: properties: order_no: @@ -273,7 +405,8 @@ definitions: maxLength: 6 type: string recipient_id: - type: integer + example: "0" + type: string recipient_username: type: string remark: @@ -285,6 +418,63 @@ definitions: - recipient_id - recipient_username type: object + redenvelope.ClaimRequest: + properties: + id: + example: "0" + type: string + required: + - id + type: object + redenvelope.CreateRequest: + properties: + cover_upload_id: + example: "0" + type: string + greeting: + maxLength: 100 + type: string + heterotypic_upload_id: + example: "0" + type: string + pay_key: + maxLength: 10 + type: string + total_amount: + type: number + total_count: + minimum: 1 + type: integer + type: + allOf: + - $ref: '#/definitions/model.RedEnvelopeType' + enum: + - fixed + - random + required: + - pay_key + - total_amount + - total_count + - type + type: object + redenvelope.ListRequest: + properties: + page: + minimum: 1 + type: integer + page_size: + maximum: 100 + minimum: 1 + type: integer + type: + enum: + - sent + - received + type: string + required: + - page + - page_size + type: object system_config.CreateSystemConfigRequest: properties: description: @@ -311,6 +501,19 @@ definitions: required: - value type: object + task.DispatchTaskRequest: + properties: + end_time: + type: string + start_time: + type: string + task_type: + type: string + user_id: + type: integer + required: + - task_type + type: object user.UpdatePayKeyRequest: properties: pay_key: @@ -319,10 +522,17 @@ definitions: required: - pay_key type: object + user.updateUserStatusRequest: + properties: + is_active: + type: boolean + type: object user_pay_config.CreateUserPayConfigRequest: properties: daily_limit: type: integer + distribute_rate: + type: number fee_rate: type: number level: @@ -330,11 +540,11 @@ definitions: max_score: type: integer min_score: - minimum: 0 type: integer score_rate: type: number required: + - distribute_rate - fee_rate - score_rate type: object @@ -342,16 +552,18 @@ definitions: properties: daily_limit: type: integer + distribute_rate: + type: number fee_rate: type: number max_score: type: integer min_score: - minimum: 0 type: integer score_rate: type: number required: + - distribute_rate - fee_rate - score_rate type: object @@ -365,7 +577,7 @@ definitions: info: contact: {} title: LINUX DO Credit - version: 0.1.0 + version: 1.0.0 paths: /api.php: get: @@ -380,16 +592,15 @@ paths: required: true type: string - in: query + maxLength: 64 + minLength: 1 name: out_trade_no + required: true type: string - in: query name: pid required: true type: string - - in: query - name: trade_no - required: true - type: integer produces: - application/json responses: @@ -418,6 +629,50 @@ paths: $ref: '#/definitions/payment.RefundMerchantOrderResponse' tags: - payment + /api/v1/admin/orders: + post: + consumes: + - application/json + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/order.listOrdersRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - admin + /api/v1/admin/orders/{id}/refund: + post: + consumes: + - application/json + parameters: + - description: 订单ID + in: path + name: id + required: true + type: integer + - description: request body + in: body + name: request + schema: + $ref: '#/definitions/order.refundOrderRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - admin /api/v1/admin/system-configs: get: produces: @@ -505,6 +760,37 @@ paths: $ref: '#/definitions/util.ResponseAny' tags: - admin + /api/v1/admin/tasks/dispatch: + post: + consumes: + - application/json + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/task.DispatchTaskRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - admin + /api/v1/admin/tasks/types: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - admin /api/v1/admin/user-pay-configs: get: produces: @@ -592,10 +878,24 @@ paths: $ref: '#/definitions/util.ResponseAny' tags: - admin - /api/v1/config/public: + /api/v1/admin/users: get: - consumes: - - application/json + parameters: + - in: query + minimum: 1 + name: page + type: integer + - in: query + maximum: 100 + minimum: 1 + name: page_size + type: integer + - in: query + name: user_id + type: integer + - in: query + name: username + type: string produces: - application/json responses: @@ -604,18 +904,178 @@ paths: schema: $ref: '#/definitions/util.ResponseAny' tags: - - config - /api/v1/health: - get: - produces: - - application/json - responses: - "200": + - admin + /api/v1/admin/users/{id}/status: + put: + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 状态 + in: body + name: request + required: true + schema: + $ref: '#/definitions/user.updateUserStatusRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - admin + /api/v1/config/public: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - config + /api/v1/dashboard/stats/daily: + get: + consumes: + - application/json + parameters: + - description: 查询天数,最大7天 + in: query + name: days + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + summary: 获取每日收支统计 + tags: + - dashboard + /api/v1/dashboard/stats/top-customers: + get: + consumes: + - application/json + parameters: + - description: 查询天数,最大7天 + in: query + name: days + required: true + type: integer + - description: 返回数量,最大10 + in: query + name: limit + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + summary: 获取Top客户 + tags: + - dashboard + /api/v1/dashboard/stats/user-balance: + get: + consumes: + - application/json + description: 统计所有用户的AvailableBalance字段 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/util.ResponseAny' + - properties: + data: + $ref: '#/definitions/dashboard.UserBalanceStatsResponse' + type: object + summary: 获取用户余额统计 + tags: + - dashboard + /api/v1/health: + get: + produces: + - application/json + responses: + "200": description: OK schema: $ref: '#/definitions/util.ResponseAny' tags: - health + /api/v1/leaderboard: + get: + consumes: + - application/json + parameters: + - description: 页码 + in: query + name: page + required: true + type: integer + - description: 每页数量 + in: query + name: page_size + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + summary: 获取排行榜列表 + tags: + - leaderboard + /api/v1/leaderboard/me: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + summary: 获取当前用户排名 + tags: + - leaderboard + /api/v1/leaderboard/users/{id}: + get: + consumes: + - application/json + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + summary: 获取指定用户排名 + tags: + - leaderboard /api/v1/merchant/api-keys: get: produces: @@ -739,7 +1199,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/link.CreatePaymentLinkRequest' + $ref: '#/definitions/link.PaymentLinkRequest' produces: - application/json responses: @@ -773,6 +1233,37 @@ paths: $ref: '#/definitions/util.ResponseAny' tags: - merchant + put: + consumes: + - application/json + parameters: + - description: API Key ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Payment Link ID + format: int64 + in: path + name: linkId + required: true + type: integer + - description: 更新支付链接请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/link.PaymentLinkRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - merchant /api/v1/merchant/payment: post: consumes: @@ -1037,6 +1528,139 @@ paths: $ref: '#/definitions/util.ResponseAny' tags: - payment + /api/v1/ready: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - health + /api/v1/redenvelope/{id}: + get: + parameters: + - description: 红包ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - redenvelope + /api/v1/redenvelope/claim: + post: + consumes: + - application/json + parameters: + - description: 领取红包请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/redenvelope.ClaimRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - redenvelope + /api/v1/redenvelope/covers: + get: + parameters: + - description: 封面类型 (cover/heterotypic) + in: query + name: type + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - redenvelope + /api/v1/redenvelope/create: + post: + consumes: + - application/json + parameters: + - description: 创建红包请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/redenvelope.CreateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - redenvelope + /api/v1/redenvelope/list: + post: + consumes: + - application/json + parameters: + - description: 列表请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/redenvelope.ListRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - redenvelope + /api/v1/upload/redenvelope/cover: + post: + consumes: + - multipart/form-data + parameters: + - description: 图片文件 + in: formData + name: file + required: true + type: file + - description: 封面类型 (cover/heterotypic) + in: formData + name: type + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - upload /api/v1/user/pay-key: put: consumes: @@ -1057,7 +1681,66 @@ paths: $ref: '#/definitions/util.ResponseAny' tags: - user + /f/{id}: + get: + parameters: + - description: Upload ID + in: path + name: id + required: true + type: string + produces: + - application/octet-stream + responses: + "200": + description: OK + tags: + - upload + /pay/distribute: + post: + consumes: + - application/json + parameters: + - description: Basic Auth (base64(client_id:client_secret)) + in: header + name: Authorization + required: true + type: string + - description: 分发请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/payment.MerchantDistributeRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - payment /pay/submit.php: + get: + consumes: + - application/x-www-form-urlencoded + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/payment.CreateOrderRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - payment post: consumes: - application/x-www-form-urlencoded diff --git a/frontend/.env.example b/frontend/.env.example index 81dd197b..a855cd4f 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,3 +1,11 @@ -LINUX_DO_PAY_BACKEND_URL=http://localhost:8010 -LINUX_DO_PAY_FRONTEND_URL=http://localhost:3010 +# 前端 URL +LINUX_DO_CREDIT_FRONTEND_URL=http://localhost:3000 + +# 后端 URL +LINUX_DO_CREDIT_BACKEND_URL=http://localhost:8000 + +# Session Cookie +LINUX_DO_CREDIT_SESSION_COOKIE_NAME=linux_do_credit_session_id + +# 环境 NODE_ENV=production \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..a411c7b6 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,52 @@ +# ---- base stage ---- +FROM node:22-alpine AS base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable && corepack prepare pnpm@10.10.0 --activate + +# ---- build stage ---- +FROM base AS builder + +ENV NODE_ENV=production +ENV NEXT_PUBLIC_LINUX_DO_CREDIT_BACKEND_URL=https://build-placeholder.invalid +ENV LINUX_DO_CREDIT_SESSION_COOKIE_NAME=__LINUX_DO_CREDIT_SESSION_COOKIE_NAME__ +ENV LINUX_DO_CREDIT_RATE_LIMIT_ENABLED=__LINUX_DO_CREDIT_RATE_LIMIT_ENABLED__ +ENV LINUX_DO_CREDIT_BACKEND_URL=https://build-placeholder-2.invalid + +ARG VERSION="" +ARG BUILD_DATE="" + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ + +RUN pnpm install --frozen-lockfile + +COPY . . + +RUN if [ -n "$VERSION" ] || [ -n "$BUILD_DATE" ]; then \ + VERSION="$VERSION" BUILD_DATE="$BUILD_DATE" node -e "const fs = require('fs'); const pkg = require('./package.json'); const version = process.env.VERSION; const buildDate = process.env.BUILD_DATE; if (version) pkg.version = version; if (buildDate) pkg.buildDate = buildDate; fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\\n')"; \ +fi + +RUN pnpm build + +RUN grep -rl \ + -e "https://build-placeholder.invalid" \ + -e "https://build-placeholder-2.invalid" \ + -e "__LINUX_DO_CREDIT_SESSION_COOKIE_NAME__" \ + -e "__LINUX_DO_CREDIT_RATE_LIMIT_ENABLED__" \ + /app/.next > /app/.replace.files + +# ---- runner stage ---- +FROM base AS runner + +WORKDIR /app + +COPY --from=builder /app . + +EXPOSE 3000 + +ENTRYPOINT ["./entrypoint.sh"] +CMD ["pnpm", "start"] diff --git a/frontend/README.md b/frontend/README.md index e215bc4c..412a6b4b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,36 +1,50 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Linux Do Credit Frontend -## Getting Started +Modern frontend application for the Linux Do Credit system. + +[中文](./README_zh.md) | English -First, run the development server: +## Tech Stack -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +- **Framework**: [Next.js 16](https://nextjs.org/) +- **Styling**: [Tailwind CSS 4](https://tailwindcss.com/) +- **UI Components**: [Radix UI](https://www.radix-ui.com/) +- **Icons**: [Lucide React](https://lucide.dev/) +- **Package Manager**: [pnpm](https://pnpm.io/) + +## Getting Started -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### Prerequisites -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +- Node.js (Latest LTS recommended) +- pnpm -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +### Installation -## Learn More +1. Install dependencies: + ```bash + pnpm install + ``` -To learn more about Next.js, take a look at the following resources: +2. Run the development server: + ```bash + pnpm dev + ``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + Open [http://localhost:3000](http://localhost:3000) (or the port shown in your terminal) with your browser to see the result. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Project Structure -## Deploy on Vercel +- `app/`: Next.js App Router pages and layouts +- `components/`: Reusable UI components + - `ui/`: Base UI components (buttons, inputs, etc.) + - `common/`: Shared business components +- `lib/`: Utility functions and service definitions +- `public/`: Static assets -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Scripts -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- `pnpm dev`: Start development server with Turbopack +- `pnpm build`: Build the application for production +- `pnpm start`: Start production server +- `pnpm lint`: Run ESLint diff --git a/frontend/README_zh.md b/frontend/README_zh.md new file mode 100644 index 00000000..33b0b12c --- /dev/null +++ b/frontend/README_zh.md @@ -0,0 +1,50 @@ +# Linux Do Credit Frontend + +Linux Do Credit 系统的现代化前端应用。 + +[English](./README.md) | 中文 + +## 技术栈 + +- **框架**: [Next.js 16](https://nextjs.org/) +- **样式**: [Tailwind CSS 4](https://tailwindcss.com/) +- **UI 组件**: [Radix UI](https://www.radix-ui.com/) +- **图标**: [Lucide React](https://lucide.dev/) +- **包管理器**: [pnpm](https://pnpm.io/) + +## 快速开始 + +### 前置要求 + +- Node.js (建议使用最新的 LTS 版本) +- pnpm + +### 安装 + +1. 安装依赖: + ```bash + pnpm install + ``` + +2. 运行开发服务器: + ```bash + pnpm dev + ``` + + 在浏览器中打开 [http://localhost:3000](http://localhost:3000) (或终端中显示的端口) 查看结果。 + +## 项目结构 + +- `app/`: Next.js App Router 页面和布局 +- `components/`: 可复用 UI 组件 + - `ui/`: 基础 UI 组件 (按钮, 输入框等) + - `common/`: 共享业务组件 +- `lib/`: 工具函数和服务定义 +- `public/`: 静态资源 + +## 脚本 + +- `pnpm dev`: 启动开发服务器 (使用 Turbopack) +- `pnpm build`: 构建生产版本 +- `pnpm start`: 启动生产服务器 +- `pnpm lint`: 运行 ESLint diff --git a/frontend/app/(auth)/privacy-policy/page.tsx b/frontend/app/(auth)/privacy-policy/page.tsx deleted file mode 100644 index 52d50d73..00000000 --- a/frontend/app/(auth)/privacy-policy/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { LegalPageLayout } from "@/components/auth/legal-page-layout"; -import { privacySections, LAST_UPDATED } from "@/components/auth/login-policies"; - -export default function PrivacyPolicyPage() { - return ( - - 感谢您使用 LINUX DO Credit。我们深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。 -

- } - /> - ); -} diff --git a/frontend/app/(auth)/terms-of-service/page.tsx b/frontend/app/(auth)/terms-of-service/page.tsx deleted file mode 100644 index 20763008..00000000 --- a/frontend/app/(auth)/terms-of-service/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { LegalPageLayout } from "@/components/auth/legal-page-layout"; -import { termsSections, LAST_UPDATED } from "@/components/auth/login-policies"; - -export default function TermsOfServicePage() { - return ( - - 欢迎使用 LINUX DO Credit。请仔细阅读以下条款,它们规定了您对我们服务的访问和使用。通过使用我们的服务,即表示您同意受这些条款的约束。如果您不同意这些条款,请立即停止使用此平台。 -

- } - /> - ); -} diff --git a/frontend/app/(docs)/docs/api/page.tsx b/frontend/app/(docs)/docs/api/page.tsx new file mode 100644 index 00000000..8ba38086 --- /dev/null +++ b/frontend/app/(docs)/docs/api/page.tsx @@ -0,0 +1,18 @@ + +import { LegalPageLayout } from "@/components/common/docs/legal-page-layout"; +import { apiSections, DOCS_LAST_UPDATED } from "@/components/common/docs/api"; + +export default function ApiDocPage() { + return ( + + LINUX DO Credit 提供简单、强大的 API 接口,支持多种编程语言和开发环境。通过标准化的 HTTP 接口,您可以轻松将积分服务集成到您的应用中。 +

+ } + /> + ); +} diff --git a/frontend/app/(docs)/docs/how-to-use/page.tsx b/frontend/app/(docs)/docs/how-to-use/page.tsx new file mode 100644 index 00000000..0724d257 --- /dev/null +++ b/frontend/app/(docs)/docs/how-to-use/page.tsx @@ -0,0 +1,19 @@ + +import { LegalPageLayout } from "@/components/common/docs/legal-page-layout"; +import { howToUseSections } from "@/components/common/docs/how-to-use"; +import { DOCS_LAST_UPDATED } from "@/components/common/docs/api"; + +export default function HowToUsePage() { + return ( + + 欢迎使用 LINUX DO Credit。本文档将引导您快速了解平台的核心功能、角色定义及使用流程,帮助您更好地使用我们的服务。 +

+ } + /> + ); +} diff --git a/frontend/app/(docs)/docs/privacy-policy/page.tsx b/frontend/app/(docs)/docs/privacy-policy/page.tsx new file mode 100644 index 00000000..50902494 --- /dev/null +++ b/frontend/app/(docs)/docs/privacy-policy/page.tsx @@ -0,0 +1,20 @@ +import { LegalPageLayout } from "@/components/common/docs/legal-page-layout" +import { privacySections } from "@/components/common/docs/privacy" +import { TERMS_LAST_UPDATED } from "@/components/common/docs/terms" + +export default function PrivacyPolicyPage() { + return ( + + 我们非常重视您的隐私保护。本政策将向您透明地展示我们如何收集、使用、存储您的个人信息, +
+ 以及您享有的相关权利。 + + } + /> + ) +} diff --git a/frontend/app/(docs)/docs/terms-of-service/page.tsx b/frontend/app/(docs)/docs/terms-of-service/page.tsx new file mode 100644 index 00000000..8725147d --- /dev/null +++ b/frontend/app/(docs)/docs/terms-of-service/page.tsx @@ -0,0 +1,19 @@ +import { LegalPageLayout } from "@/components/common/docs/legal-page-layout" +import { termsSections, TERMS_LAST_UPDATED } from "@/components/common/docs/terms" + +export default function TermsOfServicePage() { + return ( + + 欢迎使用 LINUX DO Credit。本协议详述了您在使用本平台服务时的权利与义务。 +
+ 为了保障您的合法权益,请您仔细阅读以下条款。 + + } + /> + ) +} diff --git a/frontend/app/(main)/admin/credit/page.tsx b/frontend/app/(main)/admin/credit/page.tsx new file mode 100644 index 00000000..20b4075d --- /dev/null +++ b/frontend/app/(main)/admin/credit/page.tsx @@ -0,0 +1,29 @@ +"use client" + +import { useEffect } from "react" +import { UserPayConfigs } from "@/components/common/admin/credit" +import { useUser } from "@/contexts/user-context" +import { AdminProvider, useAdmin } from "@/contexts/admin-context" + +/* 用户积分配置页面 */ +export default function UserPayConfigPage() { + return ( + + + + ) +} + +/* 用户积分配置页面内容 */ +function UserPayConfigPageContent() { + const { user } = useUser() + const { refetchUserPayConfigs } = useAdmin() + + useEffect(() => { + if (user?.is_admin) { + refetchUserPayConfigs() + } + }, [user?.is_admin, refetchUserPayConfigs]) + + return +} diff --git a/frontend/app/(main)/admin/layout.tsx b/frontend/app/(main)/admin/layout.tsx new file mode 100644 index 00000000..2cc9a5f4 --- /dev/null +++ b/frontend/app/(main)/admin/layout.tsx @@ -0,0 +1,35 @@ +"use client" + +import { AdminUsersProvider } from "@/contexts/admin-users-context" +import { useUser } from "@/contexts/user-context" +import { LoadingPage } from "@/components/layout/loading" +import { ErrorPage } from "@/components/layout/error" + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + const { user, loading } = useUser() + + /* 等待用户信息加载完成 */ + if (loading) { + return + } + + /* 权限检查:只有管理员才能访问 */ + if (!user?.is_admin) { + return ( + + ) + } + + return ( + + {children} + + ) +} diff --git a/frontend/app/(main)/admin/orders/page.tsx b/frontend/app/(main)/admin/orders/page.tsx new file mode 100644 index 00000000..c5ef16a4 --- /dev/null +++ b/frontend/app/(main)/admin/orders/page.tsx @@ -0,0 +1,7 @@ +"use client" + +import { OrdersManager } from "@/components/common/admin/orders" + +export default function OrdersPage() { + return +} diff --git a/frontend/app/(main)/admin/system/page.tsx b/frontend/app/(main)/admin/system/page.tsx index 9b65ebfc..93c1690e 100644 --- a/frontend/app/(main)/admin/system/page.tsx +++ b/frontend/app/(main)/admin/system/page.tsx @@ -1,14 +1,10 @@ "use client" import { useEffect } from "react" -import { SystemConfigs } from "@/components/common/admin/system-configs" -import { ErrorPage } from "@/components/layout/error" -import { LoadingPage } from "@/components/layout/loading" - +import { SystemConfigs } from "@/components/common/admin/system" import { useUser } from "@/contexts/user-context" import { AdminProvider, useAdmin } from "@/contexts/admin-context" - /* 系统配置页面 */ export default function SystemConfigPage() { return ( @@ -20,7 +16,7 @@ export default function SystemConfigPage() { /* 系统配置页面内容 */ function SystemConfigPageContent() { - const { user, loading } = useUser() + const { user } = useUser() const { refetchSystemConfigs } = useAdmin() useEffect(() => { @@ -29,21 +25,6 @@ function SystemConfigPageContent() { } }, [user?.is_admin, refetchSystemConfigs]) - /* 等待用户信息加载完成 */ - if (loading) { - return - } - - /* 权限检查:只有管理员才能访问 */ - if (!user?.is_admin) { - return ( - - ) - } - return } diff --git a/frontend/app/(main)/admin/tasks/page.tsx b/frontend/app/(main)/admin/tasks/page.tsx new file mode 100644 index 00000000..39e329a5 --- /dev/null +++ b/frontend/app/(main)/admin/tasks/page.tsx @@ -0,0 +1,8 @@ +"use client" + +import { TaskManager } from "@/components/common/admin/tasks" + +/* 任务管理页面 */ +export default function TasksPage() { + return +} diff --git a/frontend/app/(main)/admin/user_pay/page.tsx b/frontend/app/(main)/admin/user_pay/page.tsx deleted file mode 100644 index 38a4eb32..00000000 --- a/frontend/app/(main)/admin/user_pay/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { UserPayConfigs } from "@/components/common/admin/user-pay-configs" -import { ErrorPage } from "@/components/layout/error" -import { LoadingPage } from "@/components/layout/loading" - -import { useUser } from "@/contexts/user-context" -import { AdminProvider, useAdmin } from "@/contexts/admin-context" - - -/* 用户支付配置页面 */ -export default function UserPayConfigPage() { - return ( - - - - ) -} - -/* 用户支付配置页面内容 */ -function UserPayConfigPageContent() { - const { user, loading } = useUser() - const { refetchUserPayConfigs } = useAdmin() - - useEffect(() => { - if (user?.is_admin) { - refetchUserPayConfigs() - } - }, [user?.is_admin, refetchUserPayConfigs]) - - /* 等待用户信息加载完成 */ - if (loading) { - return - } - - /* 权限检查:只有管理员才能访问 */ - if (!user?.is_admin) { - return ( - - ) - } - - return -} diff --git a/frontend/app/(main)/admin/users/page.tsx b/frontend/app/(main)/admin/users/page.tsx new file mode 100644 index 00000000..6e20d868 --- /dev/null +++ b/frontend/app/(main)/admin/users/page.tsx @@ -0,0 +1,8 @@ +"use client" + +import { UsersManager } from "@/components/common/admin/users" + +/* 用户管理页面 */ +export default function UsersPage() { + return +} diff --git a/frontend/app/(main)/docs/api/page.tsx b/frontend/app/(main)/docs/api/page.tsx deleted file mode 100644 index 4c20b145..00000000 --- a/frontend/app/(main)/docs/api/page.tsx +++ /dev/null @@ -1,172 +0,0 @@ - -import { MarkdownRenderer } from "@/components/common/docs/markdown-renderer"; -import { TableOfContents } from "@/components/common/docs/toc"; - -const API_DOC = ` -## 1. 官方服务接口 -> 官方服务接口暂未上限,敬请期待 - ---- - - -## 2. 易支付兼容接口 -> 兼容易支付、CodePay、VPay 等支付协议 - -### 2.1 概览 -- 协议:EasyPay / CodePay / VPay 兼容协议 -- 服务类型:仅支持 \`type=epay\` -- 网关基址:\`https://pay.linux.do/epay\` -- 订单有效期:取系统配置 \`merchant_order_expire_minutes\`(平台端设置) - -### 2.2 常见错误 -- \`不支持的请求类型\`:\`type\` 仅允许 \`epay\` -- \`签名验证失败\`:参与签名字段与请求体需一致,密钥直接拼接 -- \`金额必须大于0\` / \`积分小数位数不能超过2位\` -- \`订单已过期\`:超出系统配置有效期 -- \`订单不存在或已完成\`:订单号错误、已退回或已完成 -- \`余额不足\`:余额退回时用户积分不足 - -### 2.3 对接流程 -1) 控制台创建 API Key,记录 \`pid\`、\`key\`,配置回调地址 -2) 按“签名算法”生成 \`sign\`,调用 \`/pay/submit.php\` 创建积分流转服务并跳转认证界面 -3) 可通过 \`/api.php\` 轮询结果,或等待异步回调 -4) 退回服务时,携带同一 \`trade_no\` 和原积分数量,调用积分退回接口 -5) 回调验签通过后返回 \`success\` 完成闭环 - -### 2.4 鉴权与签名 -#### 2.4.1 API Key -- \`pid\`:Client ID -- \`key\`:Client Secret(妥善保管) -- \`notify_url\`:回调地址, 使用创建应用时设置的 \`notify_url\`;请求体中的 notify_url 仅参与签名,不会覆盖创建应用时设置的 notify_url。 - -#### 2.4.2 签名算法 -1) 取所有非空字段(排除 \`sign\`、\`sign_type\` 字段) -2) 将上述字段按 ASCII 升序,依次拼成 \`k1=v1&k2=v2\` -3) 在末尾追加应用密钥:\`k1=v1&k2=v2${ "{secret}" }\` -4) 整体进行 MD5,取小写十六进制作为 \`sign\` - -示例: -\`\`\`bash -payload="money=10&name=Test&out_trade_no=M20250101&pid=001&type=epay" -sign=$(echo -n "\${payload}\${SECRET}" | md5) # 输出小写 -\`\`\` - -### 2.5 积分流转服务 -- 方法:POST \`/pay/submit.php\` -- 编码:\`application/x-www-form-urlencoded\` -- 成功:验签通过后,平台自动创建积分流转服务,并跳转到认证界面(Location=\`https://credit.linux.do/paying?order_no=...\`) -- 失败:返回 JSON \`{"error_msg":"...", "data":null}\` - -| 参数 | 必填 | 说明 | -| :-- | :-- | :-- | -| \`pid\` | 是 | Client ID | -| \`type\` | 是 | 固定 \`epay\` | -| \`out_trade_no\` | 否 | 业务单号,建议全局唯一 | -| \`name\` | 是 | 标题,最多 64 字符 | -| \`money\` | 是 | 积分数量,最多 2 位小数 | -| \`notify_url\` | 否 | 仅参与签名,不会覆盖创建应用时设置的 notify_url | -| \`return_url\` | 否 | 仅参与签名,不会覆盖创建应用时设置的 return_url | -| \`device\` | 否 | 终端标识,可选 | -| \`sign\` | 是 | 按“签名算法”生成 | -| \`sign_type\` | 否 | 固定 \`MD5\` | - -请求示例: -\`\`\`bash -curl -X POST https://credit.linux.do/epay/pay/submit.php \\ - -H "Content-Type: application/x-www-form-urlencoded" \\ - -d "pid=001" \\ - -d "type=epay" \\ - -d "out_trade_no=M20250101" \\ - -d "name=Test" \\ - -d "money=10" \\ - -d "sign=\${SIGN}" \\ - -d "sign_type=MD5" -\`\`\` - -### 2.6 订单查询 -- 方法:GET \`/api.php\` -- 认证:\`pid\` + \`key\` -- 说明:\`trade_no\` 必填;\`out_trade_no\` 可选;\`act\` 可传 \`order\`,后端不强校验。 - -| 参数 | 必填 | 说明 | -| :-- | :-- | :-- | -| \`act\` | 否 | 可选字段,建议 \`order\` | -| \`pid\` | 是 | Client ID | -| \`key\` | 是 | Client Secret | -| \`trade_no\` | 是 | 编号 | -| \`out_trade_no\` | 否 | 业务单号 | - -成功响应: -\`\`\`json -{ - "code": 1, - "msg": "查询订单号成功!", - "trade_no": "M20250101", - "out_trade_no": "M20250101", - "type": "epay", - "pid": "001", - "addtime": "2025-01-01 12:00:00", - "endtime": "2025-01-01 12:01:30", - "name": "Test", - "money": "10", - "status": 1 -} -\`\`\` - -补充:\`status\` 1=成功,0=失败/处理中;不存在会返回 HTTP 404 且 \`{"code":-1,"msg":"服务不存在或已完成"}\`。 - -### 2.7 订单退款 -- 方法:POST \`/api.php\` -- 编码:\`application/json\` 或 \`application/x-www-form-urlencoded\` -- 限制:仅支持对已成功的积分流转服务进行积分的全额退回 - -| 参数 | 必填 | 说明 | -| :-- | :-- | :-- | -| \`pid\` | 是 | Client ID | -| \`key\` | 是 | Client Secret | -| \`trade_no\` | 是 | 编号 | -| \`money\` | 是 | 必须等于原积分流转服务的积分数量 | -| \`out_trade_no\` | 否 | 业务单号(兼容) | - -响应: -\`\`\`json -{ "code": 1, "msg": "退款成功" } -\`\`\` -常见失败:服务不存在/未认证、金额不合法(<=0 或小数超过 2 位)。 - -### 2.8 异步通知(认证成功) -- 触发:认证成功后;失败自动重试,最多 5 次(单次 30s 超时) -- 目标:创建应用时设置的 notify_url -- 方式:HTTP GET - -| 参数 | 说明 | -| :-- | :-- | -| \`pid\` | Client ID | -| \`trade_no\` | 编号 | -| \`out_trade_no\` | 业务单号 | -| \`type\` | 固定 \`epay\` | -| \`name\` | 标题 | -| \`money\` | 积分数量,最多 2 位小数 | -| \`trade_status\` | 固定 \`TRADE_SUCCESS\` | -| \`sign_type\` | \`MD5\` | -| \`sign\` | 按“签名算法”生成 | - -应用需返回 HTTP 200 且响应体为 \`success\`(大小写不敏感),否则视为失败并继续重试。 -`; - -export default function ApiDocPage() { - return ( -
-
-
- -
- -
-
- ); -} diff --git a/frontend/app/(main)/docs/how-to-use/page.tsx b/frontend/app/(main)/docs/how-to-use/page.tsx deleted file mode 100644 index 9b1d06ff..00000000 --- a/frontend/app/(main)/docs/how-to-use/page.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client" - -import { MarkdownRenderer } from "@/components/common/docs/markdown-renderer" -import { TableOfContents } from "@/components/common/docs/toc" - -const CONTENT = ` -> 为社区开发者与用户提供完整的平台使用说明 - -## 1. 快速开始 -- 身份认证:基于 LINUX DO Connect (OAuth) -- 认证方式:账户积分消耗认证 -- 手续费:动态费率,由服务方承担 -- 争议处理:支持服务方与消费方的双方争议处理 - -## 2. 角色说明 -- **服务方**:最终积分流转的转入方 -- **消费方**:最终积分流转的转出方 -- **认证平台**:LINUX DO Credit 系统本身 - -## 3. 接入积分服务 -### 3.1 使用 API 接口 - -1. 创建应用 - - - 前往 [集市中心](/merchant) - - 点击顶部右侧 **创建应用** 按钮 - - 填写必要信息:应用名称、应用主页、回调地址、通知地址 - -2. 获取 API 凭证 - - 在集市中心顶部右侧选择器中选择您的应用 - - 在 **API 配置** 面板中获取: - - \`Client ID\`:客户端ID,用于标识您的身份 - - \`Client Secret\`:客户端密钥,用于签名验证(**请妥善保管,切勿泄露**) - -3. 使用 API 接口 - - 使用 API 接口创建积分流转服务 - - 参考文档:[API 接口文档](/docs/api) - -### 3.2 使用在线服务 -- 适用场景:无代码开发基础,或只用于简单的积分服务。 -- 操作步骤: - 1. 前往 [集市中心](/merchant) 创建应用,获取 API 凭证 - 2. 选择应用,点击 **在线收款** 功能 - 3. 创建在线积分服务 - 4. 获取唯一积分服务链接 - 5. 发送给您所服务的客户使用 - -### 3.3 快速集成 New API -- 适用场景:New API 站点,LINUX DO Credit 兼容 EasyPay 协议,公益站站长可直接集成。 -- 操作步骤: - 1. 前往 [集市中心](/merchant),点击 **创建应用**,填写 New API 站点信息: - | 字段 | 值 | - | :-- | :-- | - | 应用名称 | \`您的应用名称\` | - | 应用主页 | \`https://{您的 New API 域名}\` | - | 回调地址 | \`https://{您的 New API 域名}/console/log\` | - | 通知地址 | \`https://{您的 New API 域名}/api/user/epay/notify\` | - - 2. 前往 New API 站点的系统设置,找到 **支付设置**。 - - 3. 配置 LINUX DO Credit 平台参数: - | 参数 | 值 | - | :-- | :-- | - | 支付地址 | \`http://credit.linux.do/epay/pay\` | - | 易支付商户ID | 您的 \`Client ID\` | - | 易支付商户密钥 | 您的 \`Client Secret\` | - | 回调地址 | \`https://{您的 New API 域名}\` | - - 4. 配置充值方式(JSON 格式): - \`\`\`json - [ - {"color":"rgba(var(--semi-blue-5), 1)","name":"支付宝","type":"alipay"}, - {"color":"rgba(var(--semi-green-5), 1)","name":"微信","type":"wxpay"}, - {"color":"black","name":"Linux Do Credit","type":"epay"} - ] - \`\`\` - -## 4. 使用 LINUX DO Credit 积分 - -您可以在任意支持 LINUX DO Credit 的平台下使用积分。在其他平台点击使用 LINUX DO Credit 积分时,会自动跳转到 LINUX DO Credit 的积分流转服务页面,您只需要确认积分流转信息无误,并选择使用 LINUX DO Credit 进行账户认证,即可完成整个交易服务。 - - -## 5. 服务(手续)费 -### 5.1 规则说明 - -为了更好的维持 LINUX DO Credit 平台的积分服务机制,保证社区积分的生态可持续发展,我们会按照规范进行不同程度的服务(手续)费用收取。 - -- **承担方**:服务(手续)费默认**由服务方承担** -- **消费方使用**:不会产生额外费用 -- **服务方实收**:订单金额 - 服务(手续)费 - -计算公式: -\`\`\` -手续费 = 订单金额 × 当前费率 -商家实际到账 = 订单金额 - 手续费 -\`\`\` - -### 5.2 动态费率 -费率并非固定不变,会根据以下因素动态调整: -- 服务方平台等级 -- 服务方平台积分 -- LINUX DO Credit 平台活动 - -## 6. 争议处理 - -为了保障服务方与消费方的合法权益,当积分服务出现纠纷时,可使用争议功能。 - -- 作为服务方,您需要及时响应消费方的争议请求: - 1. 在集市中心或通知中查看到 **待处理的争议** - 2. 查看消费方理由,选择操作: - - **同意**:认可消费方诉求,积分原路退回给消费方 - - **拒绝**:如果您认为已履约,请提交相关证据 - -> **重要**:建议服务方与消费方优先通过沟通解决。长时间未处理的争议会由 LINUX DO Credit 平台介入仲裁,这可能会影响您的服务方信誉。 - -## 7. 积分转移 - -- 适用场景:向 LINUX DO Credit 平台内的其他用户进行积分转移。 -- 操作步骤 - 1. 进入 **活动** 页面,选择 **积分转移** - 2. 填写接收方信息: - - **接收方账户**:对方的用户名 - - **接收方 ID**:对方的用户ID,确保积分去向准确 - - **积分数量**:转移的积分数量 - 3. 验证安全密码后,积分将实时到账 - -> **重要**:请勿滥用积分转移进行刷号、交易等违规行为,积分审计系统发现此类行为会立即封禁相关账户! - -## 8. 社区积分 - -您的 LINUX DO Credit 平台基础积分主要由 **社区积分 (Community Balance)** 划转而来。 - -- **基本获取方式**:通过在 LINUX DO 社区的活跃行为获得: - - 发帖、回复、点赞等社区贡献 - - 详情可见 [LINUX DO 社区](https://linux.do/) - -- **划转规则**: - - 划转时间:社区积分每日凌晨 **00:00** 自动划转至可用余额 - - 限制说明:划转前不可用于任何积分服务 - - 服务费用:目前划转 **服务费** - -## 9. 账户设置 - -您可以在 **设置 (Settings)** 页面管理您的账户信息。 - -## 功能列表 -- **个人资料**:查看当前的账户信息和会员等级 -- **安全设置**:修改认证密码 -- **通知设置**:暂未上线 -- **外观设置**:切换页面主题、界面外观 -` - -export default function HowToUsePage() { - return ( -
-
-
- -
- -
-
- ) -} diff --git a/frontend/app/(main)/layout.tsx b/frontend/app/(main)/layout.tsx index 5af99ddb..d8361612 100644 --- a/frontend/app/(main)/layout.tsx +++ b/frontend/app/(main)/layout.tsx @@ -2,6 +2,7 @@ import { motion } from "motion/react" import { usePathname } from "next/navigation" +import { useState } from "react" import { AppSidebar } from "@/components/layout/sidebar" import { SiteHeader } from "@/components/layout/header" import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" @@ -16,6 +17,7 @@ export default function MainLayout({ children: React.ReactNode }) { const pathname = usePathname() + const [isFullWidth, setIsFullWidth] = useState(false) return ( @@ -30,9 +32,9 @@ export default function MainLayout({ > - +
-
+
+} diff --git a/frontend/app/(main)/settings/notifications/page.tsx b/frontend/app/(main)/settings/notifications/page.tsx new file mode 100644 index 00000000..f074147d --- /dev/null +++ b/frontend/app/(main)/settings/notifications/page.tsx @@ -0,0 +1,5 @@ +import { NotificationsMain } from "@/components/common/settings/notifications" + +export default function NotificationsPage() { + return +} diff --git a/frontend/app/(redenvelope)/layout.tsx b/frontend/app/(redenvelope)/layout.tsx new file mode 100644 index 00000000..9ead5ac7 --- /dev/null +++ b/frontend/app/(redenvelope)/layout.tsx @@ -0,0 +1,16 @@ +/** + * 红包页面布局 + * 提供简洁的布局,不包含侧边栏和用户上下文 + * 允许未登录用户访问红包详情 + */ +export default function RedEnvelopeLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ {children} +
+ ) +} \ No newline at end of file diff --git a/frontend/app/(redenvelope)/redenvelope/[id]/page.tsx b/frontend/app/(redenvelope)/redenvelope/[id]/page.tsx new file mode 100644 index 00000000..6ab5510a --- /dev/null +++ b/frontend/app/(redenvelope)/redenvelope/[id]/page.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next" +import { headers } from "next/headers" +import { RedEnvelopeClaimPage } from "@/components/common/redenvelope/red-envelope-claim" + +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { + const { id } = await params + const requestHeaders = await headers() + const protocol = requestHeaders.get("x-forwarded-proto") ?? "https" + const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host") ?? "" + const baseUrl = host ? `${protocol}://${host}` : "" + const title = "你收到一个LDC红包" + const description = "点击打开领取" + const url = baseUrl ? `${baseUrl}/redenvelope/${id}` : undefined + const image = baseUrl ? `${baseUrl}/red-envelope.svg` : "/red-envelope.svg" + + return { + title, + description, + openGraph: { + title, + description, + url, + type: "website", + images: [{ url: image, width: 384, height: 512, alt: "红包" }], + } + } +} + +export default async function RedEnvelopePage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + return +} \ No newline at end of file diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico index 718d6fea..df85a1c1 100644 Binary files a/frontend/app/favicon.ico and b/frontend/app/favicon.ico differ diff --git a/frontend/app/globals.css b/frontend/app/globals.css index badcf6e4..33d51423 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -133,7 +133,7 @@ } @theme inline { - /* + /* * Aurora 动画配置 */ --animate-aurora: aurora 120s linear infinite; @@ -151,4 +151,46 @@ 350% 50%; } } + + /* + * 红包动画配置 + */ + --animate-redenvelope-shake: redenvelope-shake 0.5s ease-in-out; + --animate-redenvelope-glow: redenvelope-glow 2s ease-in-out infinite; + --animate-gold-particle: gold-particle 1s ease-out forwards; + + @keyframes redenvelope-shake { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-5deg); } + 75% { transform: rotate(5deg); } + } + + @keyframes redenvelope-glow { + 0%, 100% { box-shadow: 0 0 20px rgba(255, 215, 0, 0.3); } + 50% { box-shadow: 0 0 40px rgba(255, 215, 0, 0.6); } + } + + @keyframes gold-particle { + 0% { transform: translateY(0) scale(1); opacity: 1; } + 100% { transform: translateY(-100px) scale(0); opacity: 0; } + } + + /* + * 铃铛响铃动画配置 + * 使用微妙的摆动效果,优雅且不过度 + */ + --animate-bell-ring: bell-ring 0.6s cubic-bezier(0.36, 0.07, 0.19, 0.97); + + @keyframes bell-ring { + 0% { transform: rotate(0deg); } + 10% { transform: rotate(14deg); } + 20% { transform: rotate(-12deg); } + 30% { transform: rotate(10deg); } + 40% { transform: rotate(-8deg); } + 50% { transform: rotate(6deg); } + 60% { transform: rotate(-4deg); } + 70% { transform: rotate(2deg); } + 80% { transform: rotate(-1deg); } + 100% { transform: rotate(0deg); } + } } \ No newline at end of file diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 88a5e337..fb32b7be 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,6 +3,9 @@ import { Inter, Noto_Sans_SC, Geist_Mono } from 'next/font/google'; import { Toaster } from "@/components/ui/sonner"; import { ThemeProvider } from "@/components/layout/theme-provider"; import { CustomThemeProvider } from "@/lib/theme"; +import { BellRingProvider } from "@/contexts/bell-ring-context"; +import { NotificationSettingsProvider } from "@/contexts/notification-settings-context"; +import { RiskBlockDialog } from "@/components/common/risk/risk-block-dialog"; import "./globals.css"; const inter = Inter({ @@ -49,8 +52,13 @@ export default function RootLayout({ disableTransitionOnChange > - {children} - + + + {children} + + + + diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 5d5c1149..1a383e32 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,17 +1,13 @@ "use client"; import { HeroSection } from "@/components/home/hero-section"; -import { StatsSection } from "@/components/home/stats-section"; -import { FeaturesSection } from "@/components/home/features-section"; import { DeveloperSection } from "@/components/home/developer-section"; import { FooterSection } from "@/components/home/footer-section"; export default function HomePage() { return ( -
+
- -
diff --git a/frontend/components/animate-ui/primitives/texts/counting-number.tsx b/frontend/components/animate-ui/primitives/texts/counting-number.tsx index 756b626b..b3e753f8 100644 --- a/frontend/components/animate-ui/primitives/texts/counting-number.tsx +++ b/frontend/components/animate-ui/primitives/texts/counting-number.tsx @@ -17,6 +17,7 @@ type CountingNumberProps = Omit, 'children'> & { transition?: SpringOptions; delay?: number; initiallyStable?: boolean; + thousandSeparator?: string; } & UseIsInViewOptions; function CountingNumber({ @@ -28,10 +29,11 @@ function CountingNumber({ inViewMargin = '0px', inViewOnce = true, decimalSeparator = '.', - transition = { stiffness: 90, damping: 50 }, + transition = { stiffness: 200, damping: 30 }, decimalPlaces = 0, delay = 0, initiallyStable = false, + thousandSeparator = '', ...props }: CountingNumberProps) { const { ref: localRef, isInView } = useIsInView( @@ -79,15 +81,24 @@ function CountingNumber({ const [intPart, fracPart] = formatted.split(decimalSeparator); const paddedInt = intPart?.padStart(finalIntLength, '0') ?? ''; formatted = fracPart - ? `${paddedInt}${decimalSeparator}${fracPart}` + ? `${ paddedInt }${ decimalSeparator }${ fracPart }` : paddedInt; } + if (thousandSeparator) { + const [intPart, fracPart] = formatted.split(decimalSeparator); + const regex = /\B(?=(\d{3})+(?!\d))/g; + const separatedInt = intPart.replace(regex, thousandSeparator); + formatted = fracPart + ? `${ separatedInt }${ decimalSeparator }${ fracPart }` + : separatedInt; + } + localRef.current.textContent = formatted; } }); return () => unsubscribe(); - }, [springVal, decimals, padStart, number, decimalSeparator, localRef]); + }, [springVal, decimals, padStart, number, decimalSeparator, localRef, thousandSeparator]); const finalIntLength = Math.floor(Math.abs(number)).toString().length; @@ -97,14 +108,20 @@ function CountingNumber({ if (padStart) { const [intPart, fracPart] = out.split(decimalSeparator); const paddedInt = (intPart ?? '').padStart(finalIntLength, '0'); - out = fracPart ? `${paddedInt}${decimalSeparator}${fracPart}` : paddedInt; + out = fracPart ? `${ paddedInt }${ decimalSeparator }${ fracPart }` : paddedInt; + } + if (thousandSeparator) { + const [intPart, fracPart] = out.split(decimalSeparator); + const regex = /\B(?=(\d{3})+(?!\d))/g; + const separatedInt = intPart.replace(regex, thousandSeparator); + out = fracPart ? `${ separatedInt }${ decimalSeparator }${ fracPart }` : separatedInt; } return out; }; const zeroText = padStart ? '0'.padStart(finalIntLength, '0') + - (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '') + (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '') : '0' + (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : ''); const initialText = initiallyStable ? formatValue(number) : zeroText; diff --git a/frontend/components/auth/legal-page-layout.tsx b/frontend/components/auth/legal-page-layout.tsx deleted file mode 100644 index 4701c2cb..00000000 --- a/frontend/components/auth/legal-page-layout.tsx +++ /dev/null @@ -1,201 +0,0 @@ -"use client" - -import * as React from "react" -import Link from "next/link" -import { ArrowLeft, Calendar, FileText } from "lucide-react" -import { Button } from "@/components/ui/button" -import { cn } from "@/lib/utils" -import { motion, useScroll, useSpring } from "motion/react" -import type { PolicySection } from "@/components/auth/login-policies" - -interface LegalPageLayoutProps { - title: string - lastUpdated: string - sections: PolicySection[] - description?: React.ReactNode -} - -export function LegalPageLayout({ - title, - lastUpdated, - sections, - description, -}: LegalPageLayoutProps) { - const [activeSection, setActiveSection] = React.useState(sections[0]?.value || "") - const { scrollYProgress } = useScroll() - const scaleX = useSpring(scrollYProgress, { - stiffness: 100, - damping: 30, - restDelta: 0.001 - }) - - React.useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setActiveSection(entry.target.id) - } - }) - }, - { rootMargin: "-20% 0px -35% 0px" } - ) - - sections.forEach((section) => { - const element = document.getElementById(section.value) - if (element) observer.observe(element) - }) - - return () => observer.disconnect() - }, [sections]) - - const scrollToSection = (e: React.MouseEvent, id: string) => { - e.preventDefault() - const element = document.getElementById(id) - if (element) { - const offset = 80 - const bodyRect = document.body.getBoundingClientRect().top - const elementRect = element.getBoundingClientRect().top - const elementPosition = elementRect - bodyRect - const offsetPosition = elementPosition - offset - - window.scrollTo({ - top: offsetPosition, - behavior: "smooth" - }) - } - } - - return ( -
- - -
-
-
-
- -
-
- - - - - -

- {title} -

-
- - - 最后更新:{lastUpdated} - - - - {sections.length} 个条款 - -
- {description && ( -
-
- {description} -
-
- )} -
-
- -
- - -
- {sections.map((section, index) => ( - -
- - {index + 1} - -

- {section.title.replace(/^\d+\.\s*/, '')} -

-
- -
- {section.content} -
-
- ))} - -
-
-

- 对这些条款有疑问? -

-
- - -
-
-
-
-
-
-
- ) -} diff --git a/frontend/components/auth/login-form.tsx b/frontend/components/auth/login-form.tsx index 41de0a49..d48dcb91 100644 --- a/frontend/components/auth/login-form.tsx +++ b/frontend/components/auth/login-form.tsx @@ -1,9 +1,11 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { toast } from "sonner" import { Spinner } from "@/components/ui/spinner" import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { motion, useAnimation } from "motion/react" import { Dialog, DialogContent, @@ -22,7 +24,8 @@ import { SquareArrowUpRight } from 'lucide-react'; import { cn } from "@/lib/utils" import services from "@/lib/services" -import { termsSections, privacySections } from "./login-policies" +import { termsSections } from "@/components/common/docs/terms" +import { privacySections } from "@/components/common/docs/privacy" /** @@ -42,9 +45,39 @@ export function LoginForm({ ...props }: React.ComponentProps<"div">) { const [isLoading, setIsLoading] = useState(false) + const [hasAgreed, setHasAgreed] = useState(false) + const controls = useAnimation() + + + useEffect(() => { + const agreed = localStorage.getItem("loginPromptAgreed") === "true" + if (agreed) { + setHasAgreed(true) + } + }, []) + + const handleAgreementChange = (checked: boolean | string) => { + const isChecked = checked === true + setHasAgreed(isChecked) + if (isChecked) { + localStorage.setItem("loginPromptAgreed", "true") + } else { + localStorage.removeItem("loginPromptAgreed") + } + } /* 处理登录 */ const handleLogin = async () => { + if (!hasAgreed) { + toast.error("请先阅读并勾选服务条款和隐私政策") + controls.start({ + x: [0, -4, 4, -4, 4, 0], + color: ["#ef4444", "inherit"], + transition: { duration: 0.5 } + }) + return + } + setIsLoading(true) try { await services.auth.initiateLogin() @@ -67,69 +100,83 @@ export function LoginForm({ -
-
- 登录即代表您同意我们的{" "} - - - - - - - 服务条款 - 请仔细阅读以下条款,使用本服务即表示您接受。 - - - {termsSections.map((section) => ( - - {section.title} - {section.content} - - ))} - - - - {" "}及{" "} - - - - - - - 隐私政策 - 我们重视您的隐私,以下说明信息如何收集与使用。 - - - {privacySections.map((section) => ( - - {section.title} - {section.content} - - ))} - - - - . -
+ + + +
) } diff --git a/frontend/components/auth/login-page.tsx b/frontend/components/auth/login-page.tsx index ba90671e..5bab9f08 100644 --- a/frontend/components/auth/login-page.tsx +++ b/frontend/components/auth/login-page.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { motion, AnimatePresence } from "motion/react" import { useRouter, useSearchParams } from "next/navigation" import { toast } from "sonner" @@ -12,6 +12,7 @@ import { Check } from "lucide-react" import { AuroraBackground } from "@/components/ui/aurora-background" import services from "@/lib/services" +import type { ApiResponse } from "@/lib/services/core/types" /** @@ -34,6 +35,7 @@ export function LoginPage() { const code = searchParams.get('code') return !!(state && code) }) + const [isCheckingSession, setIsCheckingSession] = useState(() => !searchParams.get('state') || !searchParams.get('code')) const [loginSuccess, setLoginSuccess] = useState(false) const [needsPayKeySetup, setNeedsPayKeySetup] = useState(false) @@ -47,6 +49,18 @@ export function LoginPage() { const isConfirmValid = confirmPayKey.length === 6 && /^\d{6}$/.test(confirmPayKey) const passwordsMatch = payKey === confirmPayKey + const resolveRedirectTarget = useCallback(() => { + const callbackUrl = searchParams.get('callbackUrl') + const storedRedirect = sessionStorage.getItem('redirect_after_login') + const target = callbackUrl || storedRedirect || '/home' + + if (storedRedirect) { + sessionStorage.removeItem('redirect_after_login') + } + + return target + }, [searchParams]) + /* 安全密码输入 */ const handlePayKeyChange = (value: string) => { const numericValue = value.replace(/\D/g, '') @@ -59,6 +73,56 @@ export function LoginPage() { setConfirmPayKey(numericValue) } + /* 登录页兜底:已登录用户直接跳转 */ + useEffect(() => { + const state = searchParams.get('state') + const code = searchParams.get('code') + + if (state && code) { + setIsCheckingSession(false) + return + } + + let cancelled = false + + const checkExistingSession = async () => { + setIsCheckingSession(true) + + try { + const response = await fetch('/api/v1/oauth/user-info', { + credentials: 'include', + cache: 'no-store', + }) + + if (cancelled) return + + if (response.ok) { + await response.json() as ApiResponse + router.replace(resolveRedirectTarget()) + return + } + + if (response.status !== 401) { + console.error('Session probe failed:', response.status) + } + } catch (error) { + if (!cancelled) { + console.error('Session probe error:', error) + } + } finally { + if (!cancelled) { + setIsCheckingSession(false) + } + } + } + + checkExistingSession() + + return () => { + cancelled = true + } + }, [router, searchParams, resolveRedirectTarget]) + /* 回调逻辑 */ useEffect(() => { const handleOAuthCallback = async () => { @@ -78,13 +142,8 @@ export function LoginPage() { setLoginSuccess(true) toast.success("登录成功") - const callbackUrl = searchParams.get('callbackUrl') || sessionStorage.getItem('redirect_after_login') || '/home' - if (sessionStorage.getItem('redirect_after_login')) { - sessionStorage.removeItem('redirect_after_login') - } - setTimeout(() => { - router.replace(callbackUrl) + router.replace(resolveRedirectTarget()) }, 1500) } } catch (error) { @@ -96,7 +155,7 @@ export function LoginPage() { } } handleOAuthCallback() - }, [searchParams, router]) + }, [searchParams, router, resolveRedirectTarget]) /* 安全密码设置 */ const handlePayKeySubmit = async (e: React.FormEvent) => { @@ -131,11 +190,7 @@ export function LoginPage() { setConfirmPayKey("") setSetupStep('password') setTimeout(() => { - const callbackUrl = searchParams.get('callbackUrl') || sessionStorage.getItem('redirect_after_login') || '/home' - if (sessionStorage.getItem('redirect_after_login')) { - sessionStorage.removeItem('redirect_after_login') - } - router.replace(callbackUrl) + router.replace(resolveRedirectTarget()) }, 1500) } catch (error) { const errorMessage = error instanceof Error ? error.message : "设置安全密码失败" @@ -180,7 +235,7 @@ export function LoginPage() { > {[0, 1, 2, 3, 4, 5].map((i) => ( - + ))} @@ -221,14 +276,14 @@ export function LoginPage() { setSetupStep('password') setConfirmPayKey('') }} - className="flex-1 h-10 rounded-full tracking-wide text-sm font-bold transition-all active:scale-95" + className="flex-1 h-9 rounded-full tracking-wide text-sm font-bold transition-all active:scale-95" > 返回 )} + + +
+ onChange({ ...values, id: value })} /> + onChange({ ...values, order_name: value })} /> + onChange({ ...values, client_id: value })} /> + onChange({ ...values, merchant_order_no: value })} /> + onChange({ ...values, payer_username: value })} /> + onChange({ ...values, payee_username: value })} /> +
+
+ + +
+
+ + ) +} + +function SearchInput({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) { + return ( +
+ + onChange(event.target.value)} className="h-8 text-xs" /> +
+ ) +} + +function OrdersTable({ + orders, + loading, + onShowDetail, + onShowDispute, + onRefund, +}: { + orders: AdminOrder[] + loading: boolean + onShowDetail: (order: AdminOrder) => void + onShowDispute: (order: AdminOrder) => void + onRefund: (order: AdminOrder) => void +}) { + return ( + + + + 名称 + 积分 + 类型 + 状态 + 积分动向 + 应用名 + 编号 + 业务单号 + 结算 + 创建时间 + 操作 + + + + {orders.map(order => { + const typeMeta = ADMIN_TYPE_CONFIG[order.type] + const statusMeta = ADMIN_STATUS_CONFIG[order.status] + const isDisputing = order.status === "disputing" + return ( + + {order.order_name} + {displayAmount(order.amount)} + + {typeMeta.label} + + + {statusMeta.label} + + + + + + {order.app_name ? ( + order.app_homepage_url ? ( + + {order.app_name} + + ) : ( + order.app_name + ) + ) : ( + "-" + )} + + {order.order_no} + {order.merchant_order_no || "-"} + {TRANSFER_STATUS_LABEL[order.payee_transfer_status] || "-"} + {formatDateTime(order.created_at)} + + + {order.dispute_id && ( + + )} + {isRefundable(order) && ( + + )} + + + ) + })} + + + ) +} + +function OrderFlow({ order }: { order: AdminOrder }) { + if (order.status === "pending" || order.status === "expired" || order.type === "community" || order.type === "red_envelope_send" || order.type === "red_envelope_refund") { + return
-
+ } + + return ( + + + +
+ + + + {userInitial(order.payer_username)} + + +
+ + + + {userInitial(order.payee_username)} + + +
+
+ +
+
+

消费方

+

ID: {order.payer_user_id}

+

账户: {order.payer_username || "-"}

+
+
+

服务方

+

ID: {order.payee_user_id}

+

账户: {order.payee_username || "-"}

+
+
+
+
+
+ ) +} + +function OrderDetailSheet({ order, onOpenChange }: { order: AdminOrder | null; onOpenChange: (open: boolean) => void }) { + return ( + + + + 订单详情 + {order?.order_no} + + {order && ( +
+
+ + + + + + + + + + + + +
+
+ )} +
+
+ ) +} + +function DetailSection({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ) +} + +function DetailGroup({ rows }: { rows: Array<[string, string]> }) { + return ( +
+ {rows.map(([label, value]) => ( +
+ {label} + {value} +
+ ))} +
+ ) +} + +function AdminDisputeDialog({ order, onOpenChange }: { order: AdminOrder | null; onOpenChange: (open: boolean) => void }) { + const disputeHistory = order?.dispute_id && order.dispute_reason && order.dispute_created_at && order.dispute_updated_at + ? { + reason: order.dispute_reason, + created_at: order.dispute_created_at, + updated_at: order.dispute_updated_at, + } + : null + const timelineStatus = order?.status === "refused" ? order.status : (order?.dispute_status || order?.status) + + return ( + + + + 争议详情 + {order ? `订单 ${ order.order_no }` : "查看争议处理记录"} + + {order && ( +
+
+
+ 争议 ID + {order.dispute_id || "-"} +
+
+ 争议状态 + {order.dispute_status ? DISPUTE_STATUS_LABEL[order.dispute_status] : "-"} +
+
+ 消费方 + {order.payer_username || "-"} +
+
+ 服务方 + {order.payee_username || "-"} +
+
+
+ + {disputeHistory && timelineStatus ? ( + + ) : ( +
+ 暂无争议内容 +
+ )} +
+
+ )} + + + +
+
+ ) +} + +function RefundDialog({ + order, + remark, + refunding, + onRemarkChange, + onOpenChange, + onConfirm, +}: { + order: AdminOrder | null + remark: string + refunding: boolean + onRemarkChange: (value: string) => void + onOpenChange: (open: boolean) => void + onConfirm: () => void +}) { + const disputeHistory = order?.dispute_id && order.dispute_reason && order.dispute_created_at && order.dispute_updated_at + ? { + reason: order.dispute_reason, + created_at: order.dispute_created_at, + updated_at: order.dispute_updated_at, + } + : null + + return ( + + + + 确认退回 + + 退回会返还消费方订单全额,商家手续费不退。该操作提交后不能二次退回。 + + + {order && ( +
+
+
+ 编号 + {order.order_no} +
+
+ 订单积分 + {displayAmount(order.amount)} +
+
+ 服务方 + {order.payee_username || "-"} +
+
+
+ + {disputeHistory ? ( + + ) : ( +
+ 该订单没有争议记录 +
+ )} +
+
+ +