羊腿Api 开发者文档 羊腿Api 开发者文档
首页
  • 入门

    • 简 介
    • 安 装
    • 快速开始
    • 返回响应码
  • 请求

    • 请求示例代码
  • API接口

    • 毒鸡汤
    • 获取IP信息归属地
    • 土味情话
    • 星座运势
    • 天气详细信息
    • 随机壁纸
    • 微博热搜实时榜单
    • 获取输入名称(测试连接)
  • 使用配置
开发者笔记
在线调试 (opens new window)
赞助
GitHub (opens new window)
首页
  • 入门

    • 简 介
    • 安 装
    • 快速开始
    • 返回响应码
  • 请求

    • 请求示例代码
  • API接口

    • 毒鸡汤
    • 获取IP信息归属地
    • 土味情话
    • 星座运势
    • 天气详细信息
    • 随机壁纸
    • 微博热搜实时榜单
    • 获取输入名称(测试连接)
  • 使用配置
开发者笔记
在线调试 (opens new window)
赞助
GitHub (opens new window)
  • API开放平台
    • 上线地址
    • 后端
    • 前端
    • 部署环境
    • 前端
    • 后端
    • 登录注册基础权限校验
      • 登录
      • 注册
      • 权限校验
      • 登录用户状态代码修改
      • 配置全局响应拦截器处理异常
    • 接口管理
    • 用户管理
    • 接口市场
    • 个人中心
    • 查看接口文档
    • 充值服务
    • 我的订单
    • 开发者文档
    • 接口管理
    • 提供一个开放接口
      • 创建一个模拟接口项目
      • 提供第一个测试接口查询名称接口
    • API签名认证
    • 开发一个SDK
    • 用户管理
    • 用户接口对应关系
    • GateWay网关
      • 创建一个新的项目
      • 添加全局过滤器
    • 解决跨项目调用方法的问题
      • 引入Dubbo和Nacos
      • 创建公共服务
    • 优化SDK
    • 引入Redis
      • 修改模块
      • 开发流程
    • 提供更多的api接口
    • 优化SDK
    • 接口请求日志记录
    • 商品信息
    • 商品订单
    • 微信Native支付
      • 前期准备工作
      • 代码
    • 网关异常处理改造
    • 商品订单状态优化
    • 登录功能增强
      • 小程序扫码登录
      • 理论基础:Oauth2.0
      • 业务逻辑
      • 邮箱登录
    • 上线前功能增强
    • 线程安全问题解决
      • 问题复现
      • 引入Redission分布式锁
      • 解决
    • 运行分析
      • 统计数据
      • 代码
    • 安装
    • 上线
      • 文档(gigotapi-doc)
      • 前端(gigotapi-fontend)
      • 后端
    • 踩坑记录
    • 本地启动步骤
    • 服务器启动步骤
    • 接口日志统计信息
    • 添加订单功能
    • 接口信息表待补全字段:
    • 金币
    • 个人中心
    • 查看接口文档
    • 网关
    • 统计
  • 开发者笔记
PYW
2023-12-12
目录

API开放平台

# 项目简介

欢迎来到我们的羊腿API开放平台,这是一个专为开发者设计的平台,我们提供一系列强大的API接口,让您能够轻松地访问和使用我们的数据。

我们的API接口涵盖了各种类型的数据,无论您是想开发一个新的应用,还是想进行数据快速获取,我们的API都能为您提供强大的支持。

为了让您能够快速上手,我们还提供了一套完整的SDK,让您能够轻松地将我们的API接入到您的应用中。

我们的开放平台致力于提供高质量、实时的数据,以满足您的各种需求。我们的API接口设计简洁易用,文档详尽,让您能够快速上手。

# 上线地址

羊腿API 接口开放平台 (opens new window)

# 官方文档

羊腿API 开发者文档 (opens new window)

# 功能介绍

功能 游客 普通用户 管理员
SDK (opens new window)快速接入 ✅ ✅ ✅
开发者API在线文档 (opens new window) ✅ ✅ ✅
邀请好友注册得坤币 ❌ ✅ ✅
微信付款 ❌ ✅ ✅
在线调试接口 ❌ ✅ ✅
每日签到得金币 ❌ ✅ ✅
接口大厅搜索接口、浏览接口 ❌ ✅ ✅
微信小程序登录 ✅ ✅ ✅
邮箱验证码登录注册 ✅ ✅ ✅
Api密钥生成/更新 ❌ ✅ ✅
钱包充值 ❌ ✅ ✅
支付成功邮箱通知(需要绑定邮箱) ❌ ✅ ✅
更新头像 ❌ ✅ ✅
绑定、换绑、解绑邮箱 ❌ ✅ ✅
取消订单、删除订单 ❌ ✅ ✅
用户管理、封号解封等 ❌ ❌ ✅
接口管理、接口发布审核、下架 ❌ ❌ ✅
运行分析 ❌ ❌ ✅

# 技术选型

# 后端

  • SpringBoot
  • SpringCloud-Gateway网关
  • Dubbo
  • Maven
  • Nacos
  • Mysql
  • Fastjson
  • Lombok
  • Junit
  • geoip2
  • Mybatis-plus
  • Hutool
  • Redis
  • Redisson
  • commons-email
  • 微信开放平台
  • 微信支付

# 前端

  • Ant Design Pro Umi
  • AntV
  • React
  • Umi
  • axios
  • VuePress
  • Hexo
  • 微信小程序

# 部署环境

  • 阿里云/腾讯云服务器
  • 腾讯云DNS解析
  • 宝塔控制面板
  • Docker
  • CentOS 7.9/Windows 11
  • JDK8
  • Nginx
  • SSL证书
  • GIT
  • XShell
  • XFTP

# 系统流程图

  • 系统架构图

    系统架构图 (1)

  • 系统运行流程图(简图)

    image-20240105132518203

  • 系统运行流程图

    系统运行流程图

# 项目初始化搭建

# 前端

  1. 安装ant design pro umi版,参考:https://panyw-git.gitee.io/2023/11/16/ant%20design%20pro%20%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA%E5%89%8D%E7%AB%AF/

  2. 删除不必要的模块信息,如国际支持等加快网站构建速度

  3. 同步后端接口,我们这儿可以使用ant自带的openapi插件,只需要在config.ts下添加一个openAPI的配置,然后导入我们的swgger文档的在线api文档地址就可以了,注意这个地方是因为swgger支持openapi规范所以可以导入

      openAPI: [
        {
          requestLibPath: "import { request } from '@umijs/max'",
          schemaPath: 'http://localhost:8090/api/v2/api-docs',
          projectName: 'gigotapi-backend',
        },
      ],
    
    1
    2
    3
    4
    5
    6
    7

# 后端

  1. 导入后端模版,后端模版提供了一些通用的配置和基础的MVC层,如Json精度丢失处理,资源放行等

    image-20231212183329155

  2. 修改yml中的基础配置

    # 公共配置文件
    # @author PYW
    # @from www.panyuwen.top
    spring:
      application:
        name: springboot-init
      # 默认 dev 环境
      profiles:
        active: dev
      # 支持 swagger3
      mvc:
        pathmatch:
          matching-strategy: ant_path_matcher
      # session 配置
      session:
        # todo 取消注释开启分布式 session(须先配置 Redis)
        # store-type: redis
        # 30 天过期
        timeout: 2592000
      # 数据库配置
      # todo 需替换配置
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/gigot_api
        username: root
        password: root
      # Redis 配置
      # todo 需替换配置,然后取消注释
    #  redis:
    #    database: 1
    #    host: localhost
    #    port: 6379
    #    timeout: 5000
    #    password: 123456
      # Elasticsearch 配置
      # todo 需替换配置,然后取消注释
    #  elasticsearch:
    #    uris: http://localhost:9200
    #    username: root
    #    password: 123456
      # 文件上传
      servlet:
        multipart:
          # 大小限制
          max-file-size: 10MB
    server:
      address: 0.0.0.0
      port: 8090
      servlet:
        context-path: /api
        # cookie 30 天过期
        session:
          cookie:
            max-age: 2592000
    mybatis-plus:
      configuration:
        map-underscore-to-camel-case: false
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
      global-config:
        db-config:
          logic-delete-field: isDelete # 全局逻辑删除的实体字段名
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    ## 微信相关
    wx:
      # 微信公众平台
      # todo 需替换配置
      mp:
        token: xxx
        aesKey: xxx
        appId: xxx
        secret: xxx
        config-storage:
          http-client-type: HttpClient
          key-prefix: wx
          redis:
            host: 127.0.0.1
            port: 6379
          type: Memory
      # 微信开放平台
      # todo 需替换配置
      open:
        appId: xxx
        appSecret: xxx
    # 对象存储
    # todo 需替换配置
    cos:
      client:
        accessKey: 
        secretKey: 
        region: 
        bucket: 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
  3. 导入基础数据库表

    # 数据库初始化
    # @author PYW
    # @from www.panyuwen.top
    
    -- 创建库
    create database if not exists gigot_api;
    
    -- 切换库
    use gigot_api;
    
    -- 用户表
    create table if not exists user
    (
        id           bigint auto_increment comment 'id' primary key,
        userAccount  varchar(256)                           not null comment '账号',
        userPassword varchar(512)                           not null comment '密码',
        unionId      varchar(256)                           null comment '微信开放平台id',
        mpOpenId     varchar(256)                           null comment '公众号openId',
        userName     varchar(256)                           null comment '用户昵称',
        userAvatar   varchar(1024)                          null comment '用户头像',
        userProfile  varchar(512)                           null comment '用户简介',
        userRole     varchar(256) default 'user'            not null comment '用户角色:user/admin/ban',
        createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
        updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
        isDelete     tinyint      default 0                 not null comment '是否删除',
        index idx_unionId (unionId)
    ) comment '用户' collate = utf8mb4_unicode_ci;
    
    -- 帖子表
    create table if not exists post
    (
        id         bigint auto_increment comment 'id' primary key,
        title      varchar(512)                       null comment '标题',
        content    text                               null comment '内容',
        tags       varchar(1024)                      null comment '标签列表(json 数组)',
        thumbNum   int      default 0                 not null comment '点赞数',
        favourNum  int      default 0                 not null comment '收藏数',
        userId     bigint                             not null comment '创建用户 id',
        createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
        updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
        isDelete   tinyint  default 0                 not null comment '是否删除',
        index idx_userId (userId)
    ) comment '帖子' collate = utf8mb4_unicode_ci;
    
    -- 帖子点赞表(硬删除)
    create table if not exists post_thumb
    (
        id         bigint auto_increment comment 'id' primary key,
        postId     bigint                             not null comment '帖子 id',
        userId     bigint                             not null comment '创建用户 id',
        createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
        updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
        index idx_postId (postId),
        index idx_userId (userId)
    ) comment '帖子点赞';
    
    -- 帖子收藏表(硬删除)
    create table if not exists post_favour
    (
        id         bigint auto_increment comment 'id' primary key,
        postId     bigint                             not null comment '帖子 id',
        userId     bigint                             not null comment '创建用户 id',
        createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
        updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
        index idx_postId (postId),
        index idx_userId (userId)
    ) comment '帖子收藏';
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
  4. pom文件,这个文件是项目做完后copy过来的,所以可以直接粘贴使用,涉及到的技术都在这儿

    (待更新)

  5. 访问swgger测试是否成功http://localhost:8090/api/doc.html#/documentManager/Settings

# 项目开发记录(前端)

# 登录注册基础权限校验

  • 这个地方基本和用户中心一致,可以参考用户中心 (opens new window)登录注册面

# 登录

  • 登录注册页面

  • Login/index.tsx

import Footer from '@/components/Footer';
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
import { LockOutlined, MobileOutlined, UserOutlined } from '@ant-design/icons';
import {
  LoginForm,
  ProFormCaptcha,
  ProFormCheckbox,
  ProFormText,
} from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { Helmet, history, Link, useModel } from '@umijs/max';
import { Alert, message, Tabs } from 'antd';
import React, { useState } from 'react';
import Settings from '../../../../config/defaultSettings';
import { userLoginUsingPost } from '@/services/gigotapi-backend/userController';
import { sleep } from '@antfu/utils';

const LoginMessage: React.FC<{
  content: string;
}> = ({ content }) => {
  return (
    <Alert
      style={{
        marginBottom: 24,
      }}
      message={content}
      type="error"
      showIcon
    />
  );
};
const Login: React.FC = () => {
  const [userLoginState] = useState<API.LoginResult>({});
  const [type, setType] = useState<string>('account');
  const {  setInitialState } = useModel('@@initialState');
  const containerClassName = useEmotionCss(() => {
    return {
      display: 'flex',
      flexDirection: 'column',
      height: '100vh',
      overflow: 'auto',
      backgroundImage:
        "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
      backgroundSize: '100% 100%',
    };
  });
  const handleSubmit = async (values: API.UserLoginRequest) => {
    try {
      // 登录
      const res = await userLoginUsingPost({
        ...values,
      });
      if (res.data) {
        const urlParams = new URL(window.location.href).searchParams;
        // 解决需要点两次登录的问题
        // 这个问题是由于 React 组件更新的异步性质引起的。
        // 在调用 setInitialState 后,状态可能并没有立即更新,而你又立即执行了 history.push
        // 可以等待后增加一个定时器解决这个问题
        await setInitialState({
          loginUser: res.data
        });
        setTimeout(() => {
          history.push(urlParams.get('redirect') || '/');
        }, 100);
        return;
      }
    } catch (error) {
      const defaultLoginFailureMessage = '登录失败,请重试!';
      console.log(error);
      message.error(defaultLoginFailureMessage);
    }
  };
  const { status, type: loginType } = userLoginState;
  return (
    <div className={containerClassName}>
      <Helmet>
        <title>
          {'登录'}- {Settings.title}
        </title>
      </Helmet>
      <div
        style={{
          flex: '1',
          padding: '32px 0',
        }}
      >
        <LoginForm
          contentStyle={{
            minWidth: 280,
            maxWidth: '75vw',
          }}
          logo={<img alt="logo" src="/logo.svg" />}
          title="羊腿Api开放平台"
          subTitle={'简单便捷,助力您的开发之旅'}
          initialValues={{
            autoLogin: true,
          }}
          onFinish={async (values) => {
            await handleSubmit(values as API.UserLoginRequest);
          }}
        >
          <Tabs
            activeKey={type}
            onChange={setType}
            centered
            items={[
              {
                key: 'account',
                label: '账户密码登录',
              },
              {
                key: 'wechat',
                label: '微信扫码登录',
              },
            ]}
          />

          {status === 'error' && loginType === 'account' && (
            <LoginMessage content={'错误的用户名和密码'} />
          )}
          {type === 'account' && (
            <>
              <ProFormText
                name="userAccount"
                fieldProps={{
                  size: 'large',
                  prefix: <UserOutlined />,
                }}
                placeholder={'请输入用户名'}
                rules={[
                  {
                    required: true,
                    message: '用户名是必填项!',
                  },
                ]}
              />
              <ProFormText.Password
                name="userPassword"
                fieldProps={{
                  size: 'large',
                  prefix: <LockOutlined />,
                }}
                placeholder={'请输入密码'}
                rules={[
                  {
                    required: true,
                    message: '密码是必填项!',
                  },
                ]}
              />
            </>
          )}

          {status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
          {type === 'mobile' && (
            <>
              <ProFormText
                fieldProps={{
                  size: 'large',
                  prefix: <MobileOutlined />,
                }}
                name="mobile"
                placeholder={'请输入手机号!'}
                rules={[
                  {
                    required: true,
                    message: '手机号是必填项!',
                  },
                  {
                    pattern: /^1\d{10}$/,
                    message: '不合法的手机号!',
                  },
                ]}
              />
              <ProFormCaptcha
                fieldProps={{
                  size: 'large',
                  prefix: <LockOutlined />,
                }}
                captchaProps={{
                  size: 'large',
                }}
                placeholder={'请输入验证码!'}
                captchaTextRender={(timing, count) => {
                  if (timing) {
                    return `${count} ${'秒后重新获取'}`;
                  }
                  return '获取验证码';
                }}
                name="captcha"
                rules={[
                  {
                    required: true,
                    message: '验证码是必填项!',
                  },
                ]}
                onGetCaptcha={async (phone) => {
                  const result = await getFakeCaptcha({
                    phone,
                  });
                  if (!result) {
                    return;
                  }
                  message.success('获取验证码成功!验证码为:1234');
                }}
              />
            </>
          )}
          <div
            style={{
              marginBottom: 24,
            }}
          >
            <ProFormCheckbox noStyle name="autoLogin">
              自动登录
            </ProFormCheckbox>
            <Link to="/user/register" style={{float: 'right'}}>新用户注册</Link>
          </div>
        </LoginForm>
      </div>
      <Footer />
    </div>
  );
};
export default Login;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226

# 注册

  • Reguster/index.tsx
import Footer from '@/components/Footer';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormText } from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { Helmet, history, Link } from '@umijs/max';
import { Alert, message, Tabs } from 'antd';
import React, { useState } from 'react';
import Settings from '../../../../config/defaultSettings';
import { userRegisterUsingPost } from '@/services/gigotapi-backend/userController';

const LoginMessage: React.FC<{
  content: string;
}> = ({ content }) => {
  return (
    <Alert
      style={{
        marginBottom: 24,
      }}
      message={content}
      type="error"
      showIcon
    />
  );
};
const Login: React.FC = () => {
  const [userLoginState] = useState<API.LoginResult>({});
  const [type, setType] = useState<string>('account');
  const containerClassName = useEmotionCss(() => {
    return {
      display: 'flex',
      flexDirection: 'column',
      height: '100vh',
      overflow: 'auto',
      backgroundImage:
        "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
      backgroundSize: '100% 100%',
    };
  });
  const handleSubmit = async (values: API.UserLoginRequest) => {
    try {
      // 注册
      const id = await userRegisterUsingPost(values);
      if (id) {
        const defaultLoginSuccessMessage = '注册成功!';
        message.success(defaultLoginSuccessMessage);
        /**
         * 方法的作用为用户未登录的界面记录到redirect参数,
         * 我们这儿是注册应该把这个参数传给登录页面让登录页处理
         *
         * @author PYW
         */
        /** 此方法会跳转到 redirect 参数所在的位置 */
        const urlParams = new URL(window.location.href).searchParams;
        const redirectUrl = urlParams.get('redirect');
        if(redirectUrl !== null){
          history.push('/user/login' + redirectUrl);
        }
        history.push('/user/login');
        return;
      }
      // 设置用户注册状态为user
    } catch (error: any) {
      const defaultLoginFailureMessage = '注册失败,请重试!';
      message.error(error.message ?? defaultLoginFailureMessage);
    }
  };
  const { status, type: loginType } = userLoginState;
  return (
    <div className={containerClassName}>
      <Helmet>
        <title>
          {'注册'}- {Settings.title}
        </title>
      </Helmet>
      <div
        style={{
          flex: '1',
          padding: '32px 0',
        }}
      >
        <LoginForm
            submitter={{searchConfig:{submitText: '注册'}}}
          contentStyle={{
            minWidth: 280,
            maxWidth: '75vw',
          }}
          logo={<img alt="logo" src="https://gigot-1315824716.cos.ap-chongqing.myqcloud.com/pictrue/logo.png" />}
          title="羊腿API开放平台"
          subTitle={'简单便捷,助力您的开发之旅'}
          initialValues={{
            autoLogin: true,
          }}
          onFinish={async (values) => {
            await handleSubmit(values as API.UserRegisterRequest);
          }}
        >
          <Tabs
            activeKey={type}
            onChange={setType}
            centered
            items={[
              {
                key: 'account',
                label: '账户密码注册',
              },
            ]}
          />

          {status === 'error' && loginType === 'account' && (
            <LoginMessage content={'错误的账号和密码'} />
          )}
          {type === 'account' && (
            <>
              <ProFormText
                name="userAccount"
                fieldProps={{
                  size: 'large',
                  prefix: <UserOutlined />,
                }}
                placeholder={'请输入账号'}
                rules={[
                  {
                    min: 2,
                    type: 'string',
                    message: '账号必须不能小于2位',
                  },
                  {
                    required: true,
                    message: '账号是必填项!',
                  },
                ]}
              />
              <ProFormText.Password
                name="userPassword"
                fieldProps={{
                  size: 'large',
                  prefix: <LockOutlined />,
                }}
                placeholder={'请输入密码'}
                rules={[
                  {
                    required: true,
                    message: '密码是必填项!',
                  },
                ]}
              />
              <ProFormText.Password
                  name="checkPassword"
                  fieldProps={{
                    size: 'large',
                    prefix: <LockOutlined />,
                  }}
                  placeholder={'请再次输入密码'}
                  rules={[
                    {
                      required: true,
                      message: '确认密码是必填项!',
                    },
                    {
                      min: 6,
                      type: 'string',
                      message: '密码必须不能小于6位',
                    },
                    ({ getFieldValue }) => ({
                      validator(_, value) {
                        if (!value || getFieldValue('userPassword') === value) {
                          return Promise.resolve();
                        }
                        return Promise.reject(new Error('两次输入的密码不一致!'));
                      },
                    }),
                  ]}
              />
            </>
          )}


          <div
            style={{
              marginBottom: 24,
            }}
          >
            <Link to="/user/login" style={{float: 'right'}}>已有账号?前往登录</Link>
          </div>
        </LoginForm>
      </div>
      <Footer />
    </div>
  );
};
export default Login;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

# 权限校验

  • src/access.ts中配置canUser和canAdmin,对应到路由中的canAdmin和canUser
/**
 * @see https://umijs.org/zh-CN/plugins/plugin-access
 * */
export default function access(initialState: Initialstate | undefined) {
  const { loginUser } = initialState ?? {};
  return {
    canUser: loginUser,
    canAdmin: loginUser && loginUser?.userRole === 'admin',
  };
}

1
2
3
4
5
6
7
8
9
10
11

# 登录用户状态代码修改

  • 因为初始存储的user属性和我们的user不一致,所以这个地方需要修改

    • 在src/typing.d.ts中添加一个初始状态,属性值为LoginUser也就是后端返回的UserVO

      declare module 'slash2';
      declare module '*.css';
      declare module '*.less';
      declare module '*.scss';
      declare module '*.sass';
      declare module '*.svg';
      declare module '*.png';
      declare module '*.jpg';
      declare module '*.jpeg';
      declare module '*.gif';
      declare module '*.bmp';
      declare module '*.tiff';
      declare module 'omit.js';
      declare module 'numeral';
      declare module '@antv/data-set';
      declare module 'mockjs';
      declare module 'react-fittext';
      declare module 'bizcharts-plugin-slider';
      
      declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
      
      interface Initialstate {
          loginUser?: API.UserVO;
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
    • 修改程序入口app.tsx中的登录校验逻辑,ps:这个地方发现修改前端样式不生效,发现是获取用户中的Setting(前端布局)信息时因为我们重写了Initialstate,其中没有包含setting因此我们直接设为默认样式就可以了

      import Footer from '@/components/Footer';
      import { Question } from '@/components/RightContent';
      import { getLoginUserUsingGet } from '@/services/gigotapi-backend/userController';
      import { LinkOutlined } from '@ant-design/icons';
      import { SettingDrawer } from '@ant-design/pro-components';
      import type { RunTimeLayoutConfig } from '@umijs/max';
      import { history, Link } from '@umijs/max';
      import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown';
      import { requestConfig } from './requestConfig';
      import Settings from "../config/defaultSettings";
      import defaultSettings from "../config/defaultSettings";
      
      const isDev = process.env.NODE_ENV === 'development';
      const loginPath = '/user/login';
      
      /**
       * 用户是否登录校验
       * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
       * */
      export async function getInitialState(): Promise<Initialstate> {
        // 当页面首次加载时,获取全局保存的数据,比如用户登录信息
        const state: Initialstate = {
          loginUser: undefined,
        };
        try {
          const res = await getLoginUserUsingGet();
          if (res.data) {
            state.loginUser = res.data;
          }
        } catch (error) {
          history.push(loginPath);
        }
        return state;
      }
      
      // ProLayout 支持的api https://procomponents.ant.design/components/layout
      export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
        return {
          actionsRender: () => [<Question key="doc" />],
          avatarProps: {
            src: initialState?.loginUser?.userName,
            title: <AvatarName />,
            render: (_, avatarChildren) => {
              return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
            },
          },
          waterMarkProps: {
            content: initialState?.loginUser?.userName,
          },
          footerRender: () => <Footer />,
          onPageChange: () => {
            const { location } = history;
            // 如果没有登录,重定向到 login
            if (!initialState?.loginUser && location.pathname !== loginPath) {
              history.push(loginPath);
            }
          },
          layoutBgImgList: [
            {
              src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
              left: 85,
              bottom: 100,
              height: '303px',
            },
            {
              src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
              bottom: -68,
              right: -45,
              height: '303px',
            },
            {
              src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
              bottom: 0,
              left: 0,
              width: '331px',
            },
          ],
          links: isDev
            ? [
                <Link key="openapi" to="/umi/plugin/openapi" target="_blank">
                  <LinkOutlined />
                  <span>OpenAPI 文档</span>
                </Link>,
              ]
            : [],
          menuHeaderRender: undefined,
          // 自定义 403 页面
          // unAccessible: <div>unAccessible</div>,
          // 增加一个 loading 的状态
          childrenRender: (children) => {
            // if (initialState?.loading) return <PageLoading />;
            return (
              <>
                {children}
                <SettingDrawer
                  disableUrlParams
                  enableDarkTheme
                  settings={defaultSettings}
                  onSettingChange={(settings) => {
                    setInitialState((preInitialState) => ({
                      ...preInitialState,
                      settings,
                    }));
                  }}
                />
              </>
            );
          },
          ...initialState?.settings,
        };
      };
      
      /**
       * @name request 配置,可以配置错误处理
       * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
       * @doc https://umijs.org/docs/max/request#配置
       */
      export const request = requestConfig;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119

# 配置全局响应拦截器处理异常

  • requestConfig.ts

    import type {RequestOptions} from '@@/plugin-request/request';
    import type {RequestConfig} from '@umijs/max';
    import {message} from 'antd';
    
    // 错误处理方案: 错误类型
    enum ErrorShowType {
      SILENT = 0,
      WARN_MESSAGE = 1,
      ERROR_MESSAGE = 2,
      NOTIFICATION = 3,
      REDIRECT = 9,
    }
    // 与后端约定的响应数据格式
    interface ResponseStructure {
      success: boolean;
      data: any;
      errorCode?: number;
      errorMessage?: string;
      showType?: ErrorShowType;
    }
    
    /**
     * @name 错误处理
     * pro 自带的错误处理, 可以在这里做自己的改动
     * @doc https://umijs.org/docs/max/request#配置
     */
    export const requestConfig: RequestConfig = {
      baseURL: 'http://localhost:8090',
      withCredentials: true,
      // 错误处理: umi@3 的错误处理方案。
      // errorConfig: {
      //   // 错误抛出
      //   errorThrower: (res) => {
      //     const { success, data, errorCode, errorMessage, showType } =
      //       res as unknown as ResponseStructure;
      //     if (!success) {
      //       const error: any = new Error(errorMessage);
      //       error.name = 'BizError';
      //       error.info = { errorCode, errorMessage, showType, data };
      //       throw error; // 抛出自制的错误
      //     }
      //   },
      //   // 错误接收及处理
      //   errorHandler: (error: any, opts: any) => {
      //     if (opts?.skipErrorHandler) throw error;
      //     // 我们的 errorThrower 抛出的错误。
      //     if (error.name === 'BizError') {
      //       const errorInfo: ResponseStructure | undefined = error.info;
      //       if (errorInfo) {
      //         const { errorMessage, errorCode } = errorInfo;
      //         switch (errorInfo.showType) {
      //           case ErrorShowType.SILENT:
      //             // do nothing
      //             break;
      //           case ErrorShowType.WARN_MESSAGE:
      //             message.warning(errorMessage);
      //             break;
      //           case ErrorShowType.ERROR_MESSAGE:
      //             message.error(errorMessage);
      //             break;
      //           case ErrorShowType.NOTIFICATION:
      //             notification.open({
      //               description: errorMessage,
      //               message: errorCode,
      //             });
      //             break;
      //           case ErrorShowType.REDIRECT:
      //             // TODO: redirect
      //             break;
      //           default:
      //             message.error(errorMessage);
      //         }
      //       }
      //     } else if (error.response) {
      //       // Axios 的错误
      //       // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
      //       message.error(`Response status:${error.response.status}`);
      //     } else if (error.request) {
      //       // 请求已经成功发起,但没有收到响应
      //       // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
      //       // 而在node.js中是 http.ClientRequest 的实例
      //       message.error('None response! Please retry.');
      //     } else {
      //       // 发送请求时出了点问题
      //       message.error('Request error, please retry.');
      //     }
      //   },
      // },
    
      // 请求拦截器
      requestInterceptors: [
        (config: RequestOptions) => {
          // 拦截请求配置,进行个性化处理。
          const url = config?.url?.concat('?token = 123');
          return { ...config, url };
        },
      ],
    
      // 响应拦截器
      responseInterceptors: [
        (response) => {
          // 拦截响应数据,进行个性化处理
          const { data } = response as unknown as ResponseStructure;
    
          if (data?.code !== 0) {
            message.error('请求失败!' + data.message);
    
          }
          return response;
        },
      ],
    };
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113

# 接口管理

  • 这儿直接列出增删改查的的代码,其实都是可以看官方文档写出来的

    • interfaceinfo

      import { interfaceMethodList, interfaceStatusList } from '@/dictionary/dictionary';
      import CreateModal from '@/pages/InterfaceInfo/components/CreateModal';
      import UpdateModal from '@/pages/InterfaceInfo/components/UpdateModal';
      import {
        addInterfaceInfoUsingPost,
        deleteByIdsInterfaceInfoUsingPost,
        editInterfaceInfoUsingPost,
        listInterfaceInfoVoByPageUsingPost,
      } from '@/services/gigotapi-backend/interfaceInfoController';
      import { PlusOutlined } from '@ant-design/icons';
      import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
      import { PageContainer, ProDescriptions, ProTable } from '@ant-design/pro-components';
      import '@umijs/max';
      import { Button, Drawer, message } from 'antd';
      import { SortOrder } from 'antd/lib/table/interface';
      import React, { useRef, useState } from 'react';
      
      const InterfaceInfoList: React.FC = () => {
        /**
         * @en-US Pop-up window of new window
         * @zh-CN 新建窗口的弹窗
         *  */
        const [createModalVisiable, handleModalOpen] = useState<boolean>(false);
        /**
         * @en-US The pop-up window of the distribution update window
         * @zh-CN 分布更新窗口的弹窗
         * */
        const [updateModalVisible, handleUpdateModalOpen] = useState<boolean>(false);
        const [showDetail, setShowDetail] = useState<boolean>(false);
        const actionRef = useRef<ActionType>();
        const [currentRow, setCurrentRow] = useState<API.InterfaceInfoVO>();
        const [selectedRowsState, setSelectedRows] = useState<API.InterfaceInfoVO[]>([]);
      
        /**
         * @en-US Update node
         * @zh-CN 更新接口
         *
         * @param fields
         */
        const handleUpdate = async (fields: API.InterfaceInfoEditRequest) => {
          const hide = message.loading('修改中...');
          if (!currentRow) {
            message.success('请选中一列');
            return;
          }
          try {
            // id的类型是index,因此不会包含在fields中,因此需要从选中列中获取
            await editInterfaceInfoUsingPost({
              id: currentRow.id,
              ...fields,
            });
            hide();
            message.success('修改成功!');
            return true;
          } catch (error) {
            hide();
            message.error('Configuration failed, please try again!');
            return false;
          }
        };
      
        /**
         *  Delete node
         * @zh-CN 删除接口
         *
         * @param selectedRows
         */
        const handleRemove = async (selectedRows: API.DeleteRequest[]) => {
          const hide = message.loading('正在删除...');
          if (!selectedRows) return true;
          try {
            await deleteByIdsInterfaceInfoUsingPost({
              ids: selectedRows.map((row) => row.id),
            });
            hide();
            message.success('删除成功');
            return true;
          } catch (error) {
            hide();
            return false;
          }
        };
      
        /**
         * @en-US Add node
         * @zh-CN 添加接口
         * @param fields
         */
        const handleAdd = async (fields: API.InterfaceInfoAddRequest) => {
          const hide = message.loading('正在添加');
          try {
            const res = await addInterfaceInfoUsingPost({
              ...fields,
            });
            hide();
            if (res.code === 0) {
              message.success('创建成功');
              handleModalOpen(false);
              // 刷新用户信息表单
              location.reload();
              return true;
            }
            return false;
          } catch (error: any) {
            hide();
            message.error('创建失败,' + error.message);
            return false;
          }
        };
      
        /**
         * @en-US International configuration
         * @zh-CN 国际化配置
         * */
        const columns: ProColumns<API.InterfaceInfoVO>[] = [
          {
            title: 'id',
            dataIndex: 'id',
            valueType: 'text',
            hideInTable: true,
            hideInForm: true,
            search: false,
          },
          {
            title: '请求类型',
            dataIndex: 'method',
            valueEnum: interfaceMethodList,
            formItemProps: {
              rules: [
                {
                  required: true,
                },
              ],
            },
          },
          {
            title: '名称',
            dataIndex: 'name',
            valueType: 'text',
            formItemProps: {
              rules: [
                {
                  required: true,
                },
              ],
            },
          },
      
          {
            title: '描述',
            dataIndex: 'description',
            valueType: 'textarea',
          },
          {
            title: '接口地址',
            dataIndex: 'url',
            valueType: 'textarea',
            formItemProps: {
              rules: [
                {
                  required: true,
                },
              ],
            },
          },
      
          {
            title: '请求头',
            dataIndex: 'requestHeader',
            valueType: 'textarea',
          },
          {
            title: '响应头',
            dataIndex: 'responseHeader',
            valueType: 'textarea',
          },
          {
            title: '接口状态',
            dataIndex: 'status',
            valueEnum: interfaceStatusList,
          },
          {
            title: '创建时间',
            dataIndex: 'createTime',
            valueType: 'dateTime',
            hideInForm: true,
          },
          {
            title: '更新时间',
            dataIndex: 'updateTime',
            valueType: 'dateTime',
            hideInForm: true,
          },
          {
            title: '操作',
            dataIndex: 'option',
            valueType: 'option',
            render: (_, record) => [
              <a
                key="config"
                onClick={() => {
                  handleUpdateModalOpen(true);
                  // setCurrentRow(record);
                }}
              >
                修改
              </a>,
            ],
          },
        ];
        return (
          <PageContainer>
            <ProTable<API.RuleListItem, API.PageParams>
              headerTitle={''}
              actionRef={actionRef}
              rowKey="id"
              search={{
                labelWidth: 120,
              }}
              toolBarRender={() => [
                <Button
                  type="primary"
                  key="primary"
                  onClick={() => {
                    handleModalOpen(true);
                  }}
                >
                  <PlusOutlined /> 新建
                </Button>,
                <Button
                  type="primary"
                  danger
                  onClick={async () => {
                    await handleRemove(selectedRowsState);
                    setSelectedRows([]);
                    actionRef.current?.reloadAndRest?.();
                  }}
                >
                  删除
                </Button>,
              ]}
              /*
                                        分页查询后端数据,这个地方是一个组件的托管,不管是刷新还是点击查询都是调用这个
                                        这儿我们直接调用的返回参数和他不一致,因此需要重写返回参数
                                      */
              request={async (
                params,
                sort: Record<string, SortOrder>,
                filter: Record<string, React.ReactText[] | null>,
              ) => {
                const res: any = await listInterfaceInfoVoByPageUsingPost({
                  ...params,
                });
                if (res?.data) {
                  return {
                    data: res?.data.records || [],
                    success: true,
                    total: res?.data.total || 0,
                  };
                } else {
                  return {
                    data: [],
                    success: false,
                    total: 0,
                  };
                }
              }}
              columns={columns}
              rowSelection={{
                onChange: (_, selectedRows) => {
                  setSelectedRows(selectedRows);
                },
              }}
            />
      
            {/*{selectedRowsState?.length > 0 && (*/}
            {/*  <FooterToolbar*/}
            {/*    extra={*/}
            {/*      <div>*/}
            {/*        已选择{' '}*/}
            {/*        <a*/}
            {/*          style={{*/}
            {/*            fontWeight: 600,*/}
            {/*          }}*/}
            {/*        >*/}
            {/*          {selectedRowsState.length}*/}
            {/*        </a>{' '}*/}
            {/*        项 &nbsp;&nbsp;*/}
            {/*        <span>*/}
            {/*          服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)} 万*/}
            {/*        </span>*/}
            {/*      </div>*/}
            {/*    }*/}
            {/*  >*/}
            {/*    <Button*/}
            {/*      onClick={async () => {*/}
            {/*        await handleRemove(selectedRowsState);*/}
            {/*        setSelectedRows([]);*/}
            {/*        actionRef.current?.reloadAndRest?.();*/}
            {/*      }}*/}
            {/*    >*/}
            {/*      批量删除*/}
            {/*    </Button>*/}
            {/*  </FooterToolbar>*/}
            {/*)}*/}
      
            <Drawer
              width={600}
              open={showDetail}
              onClose={() => {
                setCurrentRow(undefined);
                setShowDetail(false);
              }}
              closable={false}
            >
              {currentRow?.name && (
                <ProDescriptions<API.RuleListItem>
                  column={2}
                  title={currentRow?.name}
                  request={async () => ({
                    data: currentRow || {},
                  })}
                  params={{
                    id: currentRow?.name,
                  }}
                  columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
                />
              )}
            </Drawer>
      
            <CreateModal
              columns={columns}
              onCancel={() => {
                handleModalOpen(false);
              }}
              onSubmit={(values) => {
                handleAdd(values);
              }}
              visible={createModalVisiable}
            />
            <UpdateModal
              columns={columns}
              onSubmit={async (value) => {
                const success = await handleUpdate(value);
                if (success) {
                  handleUpdateModalOpen(false);
                  setCurrentRow(undefined);
                  if (actionRef.current) {
                    actionRef.current.reload();
                  }
                }
              }}
              onCancel={() => {
                handleUpdateModalOpen(false);
                if (!showDetail) {
                  setCurrentRow(undefined);
                }
              }}
              visible={updateModalVisible}
              values={currentRow || {}}
            />
          </PageContainer>
        );
      };
      export default InterfaceInfoList;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
    • CreateModal.tsx

      import { ProColumns, ProTable } from '@ant-design/pro-components';
      import '@umijs/max';
      import { Modal } from 'antd';
      import React from 'react';
      
      export type CreateFormProps = {
        columns: ProColumns<API.InterfaceInfoVO>[];
        onCancel: () => void;
        // 当用户提交表单,将用户输入的信息传递后台
        onSubmit: (values: API.InterfaceInfoAddRequest) => Promise<void>;
        // 是否可见
        visible: boolean;
        // 不用传递
        // values: Partial<API.RuleListItem>;
      };
      const CreateModal: React.FC<CreateFormProps> = (props) => {
          // 使用解构赋值获取props中的属性
          const {visible, columns, onCancel, onSubmit} = props;
      
      
          return (
          // 创建一个modal组件,通过visible控制是否隐藏,footer设置为null把表单的确认和取消按钮去掉
          <Modal
              visible={visible} footer={null} onCancel={() => onCancel?.()}
          >
              {/*   创建一个protable组件,设定它的类型为表单,通过columns属性设置表格的列,提交表单调用onSubmit方法 */}
              <ProTable
                  headerTitle={'新建接口'}
                  type="form"
                  columns={columns}
                  onSubmit={async (value) => {
                      onSubmit?.(value);
                  }}
              />
      
          </Modal>
      );
      };
      export default CreateModal;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
    • UpdateModal.tsx

      import type { ProColumns, ProFormInstance } from '@ant-design/pro-components';
      import { ProTable } from '@ant-design/pro-components';
      import '@umijs/max';
      import { Modal } from 'antd';
      import React, { useEffect, useRef } from 'react';
      
      export type Props = {
        values: API.InterfaceInfoEditRequest;
        columns: ProColumns<API.InterfaceInfoVO>[];
        onCancel: () => void;
        onSubmit: (values: API.InterfaceInfoEditRequest) => Promise<void>;
        visible: boolean;
      };
      
      const UpdateModal: React.FC<Props> = (props) => {
          const { values, visible, columns, onCancel, onSubmit } = props;
      
          const formRef = useRef<ProFormInstance>();
      
          useEffect(() => {
              if (formRef) {
                  formRef.current?.setFieldsValue(values);
              }
          }, [values])
      
          return (
              <Modal visible={visible} footer={null} onCancel={() => onCancel?.()}>
                  <ProTable
                      type="form"
                      formRef={formRef}
                      columns={columns}
                      onSubmit={async (value) => {
                          onSubmit?.(value);
                      }}
                  />
              </Modal>
          );
      };
      export default UpdateModal;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40

# 用户管理

已经开发过很多次了和接口管理类似,因此直接上源码

  • UserManage/index.tsx

    import { userRoleList } from '@/enum/userEnum';
    import CreateModal from '@/pages/Admin/UserManage/components/CreateModal';
    import UpdateModal from '@/pages/Admin/UserManage/components/UpdateModal';
    import {
      offlineInterfaceInfoUsingPost,
      onlineInterfaceInfoUsingPost,
    } from '@/services/gigotapi-backend/interfaceInfoController';
    import {
      addUserUsingPost,
      deleteUserByIdsUsingPost,
      listUserVoByPageUsingPost,
      updateUserUsingPost,
    } from '@/services/gigotapi-backend/userController';
    import { PlusOutlined } from '@ant-design/icons';
    import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
    import { PageContainer, ProDescriptions, ProTable } from '@ant-design/pro-components';
    import '@umijs/max';
    import { Button, Drawer, message , Image } from 'antd';
    import { SortOrder } from 'antd/lib/table/interface';
    import React, { useRef, useState } from 'react';
    
    const UserList: React.FC = () => {
      /**
       * @en-US Pop-up window of new window
       * @zh-CN 新建窗口的弹窗
       *  */
      const [createModalVisiable, handleModalOpen] = useState<boolean>(false);
      /**
       * @en-US The pop-up window of the distribution update window
       * @zh-CN 分布更新窗口的弹窗
       * */
      const [updateModalVisible, handleUpdateModalOpen] = useState<boolean>(false);
      const [showDetail, setShowDetail] = useState<boolean>(false);
      const actionRef = useRef<ActionType>();
      const [currentRow, setCurrentRow] = useState<API.UserVO>();
      const [selectedRowsState, setSelectedRows] = useState<API.UserVO[]>([]);
    
      /**
       * @en-US Update node
       * @zh-CN 更新用户
       *
       * @param fields
       */
      const handleUpdate = async (fields: API.UserUpdateRequest) => {
        const hide = message.loading('修改中...');
        if (!currentRow) {
          message.success('请选中一列');
          return;
        }
        try {
          // id的类型是index,因此不会包含在fields中,因此需要从选中列中获取
          await updateUserUsingPost({
            id: currentRow.id,
            ...fields,
          });
          hide();
          message.success('修改成功!');
          return true;
        } catch (error) {
          hide();
          message.error('修改失败,请再试一次');
          return false;
        }
      };
    
      /**
       *  Delete node
       * @zh-CN 删除用户
       *
       * @param selectedRows
       */
      const handleRemove = async (selectedRows: API.DeleteRequest[]) => {
        const hide = message.loading('正在删除...');
        if (!selectedRows) return true;
        try {
          await deleteUserByIdsUsingPost({
            ids: selectedRows.map((row) => row.id),
          });
          hide();
          message.success('删除成功');
          return true;
        } catch (error) {
          hide();
          return false;
        }
      };
    
      /**
       *  online node
       * @zh-CN 禁用用户
       *
       * @param fields
       */
      const handleOnLine = async (fields: API.IdRequest) => {
        const hide = message.loading('正在发布...');
        if (!currentRow) {
          message.error('未查询到当前行信息');
        }
        try {
          await onlineInterfaceInfoUsingPost({
            id: fields.id,
          });
          hide();
          message.success('发布成功');
          return true;
        } catch (error) {
          hide();
          return false;
        }
      };
    
      /**
       *  online node
       * @zh-CN 解除禁用用户
       *
       * @param fields
       */
      const handleOffLine = async (fields: API.IdRequest) => {
        const hide = message.loading('正在下线...');
        if (!currentRow) {
          message.error('未查询到当前行信息');
        }
        try {
          await offlineInterfaceInfoUsingPost({
            id: fields.id,
          });
          hide();
          message.success('下线成功');
          return true;
        } catch (error) {
          hide();
          return false;
        }
      };
    
      /**
       * @en-US Add node
       * @zh-CN 添加用户
       * @param fields
       */
      const handleAdd = async (fields: API.UserAddRequest) => {
        const hide = message.loading('正在添加');
        try {
          const res = await addUserUsingPost({
            ...fields,
          });
          hide();
          if (res.code === 0) {
            message.success('创建成功');
            handleModalOpen(false);
            // 刷新用户信息表单
            location.reload();
            return true;
          }
          return false;
        } catch (error: any) {
          hide();
          message.error('创建失败,' + error.message);
          return false;
        }
      };
    
      // @ts-ignore
        /**
       * @en-US International configuration
       * @zh-CN 国际化配置
       * */
      const columns: ProColumns<API.UserVO>[] = [
        {
          title: 'id',
          dataIndex: 'id',
          valueType: 'text',
          hideInTable: true,
          hideInForm: true,
          search: false,
        },
        {
          title: '用户账号',
          dataIndex: 'userAccount',
          valueType: 'text',
          hideInTable: true,
          hideInSearch: true,
          formItemProps: {
            rules: [
              {
                required: true,
              },
            ],
          },
        },
    
        {
          title: '用户昵称',
          dataIndex: 'userName',
          valueType: 'text',
          formItemProps: {
            rules: [
              {
                required: true,
              },
            ],
          },
        },
        {
          title: '头像',
          dataIndex: 'userAvatar',
          search: false,
          hideInForm: true,
          render: (_, record) => (
            <div>
              <Image src={record.userAvatar} width={100} />
            </div>
          ),
        },
        {
          title: '用户简介',
          dataIndex: 'userProfile',
          valueType: 'textarea',
        },
        {
          title: '用户角色',
          dataIndex: 'userRole',
          valueType: 'select',
          valueEnum: userRoleList,
        },
        {
          title: '操作',
          dataIndex: 'option',
          valueType: 'option',
          render: (_, record) => [
            <a
              onClick={() => {
                handleUpdateModalOpen(true);
                setCurrentRow(record);
              }}
            >
              修改
            </a>,
            record.userRole === 'ban' ? (
              <a
                onClick={() => {
                  handleOnLine(record);
                  setCurrentRow(record);
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                解除
              </a>
            ) : (
              <a
                onClick={() => {
                  handleOffLine(record);
                  setCurrentRow(record);
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                封禁
              </a>
            ),
          ],
        },
      ];
      return (
        <PageContainer>
          <ProTable<API.RuleListItem, API.PageParams>
            headerTitle={''}
            actionRef={actionRef}
            rowKey="id"
            search={{
              labelWidth: 120,
            }}
            toolBarRender={() => [
              <Button
                type="primary"
                key="primary"
                onClick={() => {
                  handleModalOpen(true);
                }}
              >
                <PlusOutlined /> 新建
              </Button>,
              <Button
                type="primary"
                danger
                onClick={async () => {
                  await handleRemove(selectedRowsState);
                  setSelectedRows([]);
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                删除
              </Button>,
            ]}
            /*
                                                                                              分页查询后端数据,这个地方是一个组件的托管,不管是刷新还是点击查询都是调用这个
                                                                                              这儿我们直接调用的返回参数和他不一致,因此需要重写返回参数
                                                                                            */
            request={async (
              params,
              sort: Record<string, SortOrder>,
              filter: Record<string, React.ReactText[] | null>,
            ) => {
              const res: any = await listUserVoByPageUsingPost({
                ...params,
              });
              if (res?.data) {
                return {
                  data: res?.data.records || [],
                  success: true,
                  total: res?.data.total || 0,
                };
              } else {
                return {
                  data: [],
                  success: false,
                  total: 0,
                };
              }
            }}
            columns={columns}
            rowSelection={{
              onChange: (_, selectedRows) => {
                setSelectedRows(selectedRows);
              },
            }}
          />
    
          {/*{selectedRowsState?.length > 0 && (*/}
          {/*  <FooterToolbar*/}
          {/*    extra={*/}
          {/*      <div>*/}
          {/*        已选择{' '}*/}
          {/*        <a*/}
          {/*          style={{*/}
          {/*            fontWeight: 600,*/}
          {/*          }}*/}
          {/*        >*/}
          {/*          {selectedRowsState.length}*/}
          {/*        </a>{' '}*/}
          {/*        项 &nbsp;&nbsp;*/}
          {/*        <span>*/}
          {/*          服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)} 万*/}
          {/*        </span>*/}
          {/*      </div>*/}
          {/*    }*/}
          {/*  >*/}
          {/*    <Button*/}
          {/*      onClick={async () => {*/}
          {/*        await handleRemove(selectedRowsState);*/}
          {/*        setSelectedRows([]);*/}
          {/*        actionRef.current?.reloadAndRest?.();*/}
          {/*      }}*/}
          {/*    >*/}
          {/*      批量删除*/}
          {/*    </Button>*/}
          {/*  </FooterToolbar>*/}
          {/*)}*/}
    
          <Drawer
            width={600}
            open={showDetail}
            onClose={() => {
              setCurrentRow(undefined);
              setShowDetail(false);
            }}
            closable={false}
          >
            {currentRow?.name && (
              <ProDescriptions<API.RuleListItem>
                column={2}
                title={currentRow?.name}
                request={async () => ({
                  data: currentRow || {},
                })}
                params={{
                  id: currentRow?.name,
                }}
                columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
              />
            )}
          </Drawer>
    
          <CreateModal
            columns={columns}
            onCancel={() => {
              handleModalOpen(false);
            }}
            onSubmit={(values) => {
              handleAdd(values);
            }}
            visible={createModalVisiable}
          />
          <UpdateModal
            /*  排除dataIndex为userAccount的元素*/
            columns={columns.filter((column) => column.dataIndex !== 'userAccount')}
            onSubmit={async (value) => {
              const success = await handleUpdate(value);
              if (success) {
                handleUpdateModalOpen(false);
                setCurrentRow(undefined);
                if (actionRef.current) {
                  actionRef.current.reload();
                }
              }
            }}
            onCancel={() => {
              handleUpdateModalOpen(false);
              if (!showDetail) {
                setCurrentRow(undefined);
              }
            }}
            visible={updateModalVisible}
            values={currentRow || {}}
          />
        </PageContainer>
      );
    };
    export default UserList;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    • UserManage/components/CreateModal.tsx

      import { ProColumns, ProTable } from '@ant-design/pro-components';
      import '@umijs/max';
      import { Modal } from 'antd';
      import React from 'react';
      
      export type CreateFormProps = {
        columns: ProColumns<API.InterfaceInfoVO>[];
        onCancel: () => void;
        // 当用户提交表单,将用户输入的信息传递后台
        onSubmit: (values: API.InterfaceInfoAddRequest) => Promise<void>;
        // 是否可见
        visible: boolean;
        // 不用传递
        // values: Partial<API.RuleListItem>;
      };
      const CreateModal: React.FC<CreateFormProps> = (props) => {
          // 使用解构赋值获取props中的属性
          const {visible, columns, onCancel, onSubmit} = props;
      
      
          return (
          // 创建一个modal组件,通过visible控制是否隐藏,footer设置为null把表单的确认和取消按钮去掉
          <Modal
              visible={visible} footer={null} onCancel={() => onCancel?.()}
          >
              {/*   创建一个protable组件,设定它的类型为表单,通过columns属性设置表格的列,提交表单调用onSubmit方法 */}
              <ProTable
                  headerTitle={'新建接口'}
                  type="form"
                  columns={columns}
                  onSubmit={async (value) => {
                      onSubmit?.(value);
                  }}
              />
      
          </Modal>
      );
      };
      export default CreateModal;
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
    • UserManage/components/UpdateModal.tsx

      import { uploadAvatarUsingPost } from '@/services/gigotapi-backend/fileController';
      import {
        ProColumns,
        ProFormInstance,
        ProFormUploadButton,
        ProTable,
      } from '@ant-design/pro-components';
      import '@umijs/max';
      import {message, Modal} from 'antd';
      import { RcFile } from 'antd/lib/upload';
      import React, { useEffect, useRef } from 'react';
      
      export type Props = {
        values: API.UserUpdateRequest;
        columns: ProColumns<API.UserVO>[];
        onCancel: () => void;
        onSubmit: (Values: API.UserUpdateRequest) => Promise<void>;
        visible: boolean;
      };
      
      const UpdateModal: React.FC<Props> = (props) => {
        const { values, visible, columns, onCancel, onSubmit } = props;
      
        const formRef = useRef<ProFormInstance>();
      
        /**
         * 上传文件前校验
         * @param file
         */
        const beforeUpload = (file: RcFile) => {
          // 检查文件类型是否为 JPEG 或 PNG
          const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
      
          if (!isJpgOrPng) {
            // 如果不是 JPEG 或 PNG,显示错误消息并阻止上传
            message.error('只允许上传 JPG/PNG 格式的文件');
            return false;
          }
      
          // 检查文件大小是否小于 2MB
          const isLt2M = file.size / 1024 / 1024 < 2;
      
          if (!isLt2M) {
            // 如果文件大小超过 2MB,显示错误消息并阻止上传
            message.error('仅支持2M以下的文件');
            return false;
          }
      
          // 如果通过了上述条件,允许上传
          return true;
        };
      
        useEffect(() => {
          if (formRef) {
            formRef.current?.setFieldsValue(values);
          }
        }, [values]);
      
        return (
          <Modal
            title="编辑用户"
            visible={visible}
            footer={null}
            onCancel={() => onCancel?.()}
            // centered
            style={{ padding: 24 }}
          >
            <ProFormUploadButton
              name="avatarUrl"
              label="头像"
              listType="picture-card"
              max={1}
              fieldProps={{
                multiple: false,
                name: 'file',
                accept: 'image/png, image/jpeg',
      
                headers: { timestamp: new Date().valueOf() + '' },
                beforeUpload: (file) => {
                  beforeUpload(file);
                },
              }}
              action={async (file: RcFile) => {
                const result = await uploadAvatarUsingPost(file);
                // result就是图片名称
                const apiUrl =
                  process.env.NODE_ENV === 'development'
                    ? 'http://localhost:8090'
                    : 'http://8.137.55.140';
                values.userAvatar = apiUrl + '/api/file/downloadAvatar?name=' + result.data;
                // result就是图片名称
                // let getImgName = getResult.data;
                // alert(getImgName)
                // imageUrl = await downloadAvatar({name});
              }}
              extra="只能上传jpg/png文件,且不大于2MB"
            />
            <ProTable
              type="form"
              formRef={formRef}
              columns={columns}
              onSubmit={async (value: API.UserUpdateRequest) => {
                value.userAvatar = values.userAvatar;
                onSubmit?.(value);
              }}
            />
          </Modal>
        );
      };
      export default UpdateModal;
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110

# 接口市场

  • 使用ProList搭建,其实和Protable使用比较类似

    • index.tsx
    import { interfaceMethodList } from '@/enum/interfaceInfoEnum';
    import { listInterfaceInfoVoByPageUsingPost } from '@/services/gigotapi-backend/interfaceInfoController';
    import { PageContainer, ProList, ProListMeta } from '@ant-design/pro-components';
    import { Progress, Space, Tag } from 'antd';
    import { SortOrder } from 'antd/lib/table/interface';
    import React, { Key, useEffect, useState } from 'react';
    
    const ListOpenApi: React.FC = () => {
      const [loading, setLoading] = useState(false);
      const [list, setList] = useState<API.InterfaceInfoVO[]>([]);
      const [nowCurrent, setNowCurrent] = useState<'1'>();
      const [nowPageSize, setNowPageSize] = useState<'10'>();
      const [nowTotal, setNowTotal] = useState<string>();
    
      // matas
      const matas: ProListMeta<API.InterfaceInfoVO> = {
        subTitle: {
          title: '请求方式',
          dataIndex: 'method',
          valueType: 'select',
          valueEnum: interfaceMethodList,
          render: (_, record) => {
            if (record.method === interfaceMethodList.GET.text) {
              return (
                <Space size={0}>
                  <Tag color="blue">GET</Tag>
                </Space>
              );
            }
            if (record.method === interfaceMethodList.POST.text) {
              return (
                <Space size={0}>
                  <Tag color="green">POST</Tag>
                </Space>
              );
            }
            if (record.method === interfaceMethodList.DELETE.text) {
              return (
                <Space size={0}>
                  <Tag color="red">DELETE</Tag>
                </Space>
              );
            }
            if (record.method === interfaceMethodList.PUT.text) {
              return (
                <Space size={0}>
                  <Tag color="orange">PUT</Tag>
                </Space>
              );
            }
          },
        },
        title: {
          title: '名称',
          valueType: 'text',
          dataIndex: 'name',
          render: (_, record) => {
            return record.name;
          },
        },
        description: {
          title: '描述',
          valueType: 'text',
          dataIndex: 'description',
          ellipsis: true,
          render: (_, record) => {
            return record.description;
          },
        },
        content: {
          title: '操作',
          search: false,
          render: () => (
            <div
              style={{
                minWidth: 200,
                flex: 1,
                display: 'flex',
                justifyContent: 'flex-end',
              }}
            >
              <div
                style={{
                  width: '200px',
                }}
              >
                <div>成功率</div>
                <Progress percent={80} />
              </div>
            </div>
          ),
        },
        actions: {
          render: (_, record) => {
              const apiLink = `/ListOpenApi/info/${record.id}`
            return <a key={record.id} href={apiLink}>查看</a>;
          },
        },
      };
    
      /**
       * 加载数据
       * @param values
       */
      const loadData = async (values: API.InterfaceInfoQueryRequest) => {
        const res = await listInterfaceInfoVoByPageUsingPost(values);
        setNowTotal(res?.data?.total);
        const records = res?.data?.records ?? [];
        setList(records);
        return res;
      };
    
      const flushData = async (
        params,
        sort: Record<string, SortOrder>,
        filter: Record<string, React.ReactText[] | null>,
      ) => {
        const res = await listInterfaceInfoVoByPageUsingPost({ ...params });
        const records = res?.data?.records ?? [];
        if (res?.data) {
          setList(records);
          // @ts-ignore
          setNowTotal(res?.data?.total || 0);
          return {
            data: res?.data.records || [],
            success: true,
            total: res?.data.total || 0,
          };
        } else {
          return {
            data: [],
            success: false,
            total: 0,
          };
        }
      };
    
      useEffect(() => {
        // 页面加载完成后调用加载数据的函数
        loadData({ current: nowCurrent, pageSize: nowPageSize });
      }, []);
    
      const [expandedRowKeys, setExpandedRowKeys] = useState<readonly Key[]>([]);
      return (
        <PageContainer>
          <ProList<API.InterfaceInfoVO>
            loading={loading}
            pagination={{
              showSizeChanger: true,
              defaultCurrent: 1,
              defaultPageSize: 10,
              total: Number(nowTotal),
              pageSizeOptions: ['5', '10', '15', '20'],
              onChange(current, pageSize) {
                setNowCurrent(current + 1);
                setNowPageSize(pageSize);
              },
            }}
            search={{
              labelWidth: 120,
            }}
            request={flushData}
            rowKey="title"
            headerTitle=""
            split={true}
            // 控制支持展开
            // expandable={{ expandedRowKeys, onExpandedRowsChange: setExpandedRowKeys }}
            dataSource={list}
            // 设置matas 类似coloum的设置
            metas={matas}
          />
        </PageContainer>
      );
    };
    
    export default ListOpenApi;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
  • 列表写完之后,用户还需要调看查询信息,因此需要跳转到一个新的链接,这个页面主要使用card组件显示内容,后续的接口调用测试都在这儿,现在先写展示内容后续再添加

    import { interfaceStatusList } from '@/enum/interfaceInfoEnum';
    import { getInterfaceInfoVoByIdUsingGet } from '@/services/gigotapi-backend/interfaceInfoController';
    import { useParams } from '@@/exports';
    import { PageContainer } from '@ant-design/pro-components';
    import { Badge, Card, Descriptions, message, Typography } from 'antd';
    import React, { useEffect, useState } from 'react';
    
    
    
    const ListOpenApiInfo: React.FC = () => {
      // 定义状态和钩子函数
      const [loading, setLoading] = useState(false);
      const [data, setData] = useState<API.InterfaceInfoVO>();
      const params = useParams();
    
      const loadData = async () => {
        if (!params.id) {
          message.error('参数不存在');
          return;
        }
        setLoading(true);
        try {
          const res = await getInterfaceInfoVoByIdUsingGet({
            id: params.id,
          });
          setData(res.data);
        } catch (error: any) {
          message.error('请求失败,' + error.message);
        }
        setLoading(false);
      };
    
      useEffect(() => {
        loadData();
      }, []);
    
      return (
        <PageContainer
          header={{
            breadcrumb: {},
          }}
        >
          <Card>
            {data ? (
              <Descriptions title={data.name} column={3}>
                <Descriptions.Item label="描述">{data.description}</Descriptions.Item>
                <Descriptions.Item label="请求地址">{data.url}</Descriptions.Item>
                <Descriptions.Item label="请求方法">{data.method}</Descriptions.Item>
                <Descriptions.Item label="接口状态">
                  {data && data.status === 0 ? (
                    <Badge
                      status={interfaceStatusList['0'].status}
                      text={interfaceStatusList['0'].text}
                    />
                  ) : null}
                  {data && data.status === 1 ? (
                    <Badge
                      status={interfaceStatusList['1'].status}
                      text={interfaceStatusList['1'].text}
                    />
                  ) : null}
                </Descriptions.Item>
              </Descriptions>
            ) : (
              <>接口不存在</>
            )}
          </Card>
        </PageContainer>
      );
    };
    
    export default ListOpenApiInfo;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73

# 个人中心

  • 这儿主要使用ProCard的拼接,然后使用了一个切割头像的插件,并且把用户的api密钥放在里面,后续还需要添加钱包,下载SDK等功能

    import { userRoleList } from '@/enum/userEnum';
    import { requestConfig } from '@/requestConfig';
    import { uploadAvatarUsingPost } from '@/services/gigotapi-backend/fileController';
    import {
      getLoginUserUsingGet,
      getVoucherUsingGet,
      updateUserUsingPost,
      updateVoucherUsingPost,
    } from '@/services/gigotapi-backend/userController';
    import { EditOutlined, PlusOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
    import { PageContainer, ProCard } from '@ant-design/pro-components';
    import { Button, Descriptions, message, Modal, Upload, UploadFile, UploadProps } from 'antd';
    import ImgCrop from 'antd-img-crop';
    import Paragraph from 'antd/lib/typography/Paragraph';
    import { RcFile } from 'antd/lib/upload';
    import React, { useEffect, useState } from 'react';
    
    /**
     * 校验值是否为空
     * @param val
     */
    export const valueLength = (val: any) => {
      return val && val.trim().length > 0;
    };
    /**
     * 上传前校验
     * @param file
     */
    const beforeUpload = (file: RcFile) => {
      // 检查文件类型是否为 JPEG 或 PNG
      const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    
      if (!isJpgOrPng) {
        // 如果不是 JPEG 或 PNG,显示错误消息并阻止上传
        message.error('只允许上传 JPG/PNG 格式的文件');
        return false;
      }
    
      // 检查文件大小是否小于 2MB
      const isLt2M = file.size / 1024 / 1024 < 2;
    
      if (!isLt2M) {
        // 如果文件大小超过 2MB,显示错误消息并阻止上传
        message.error('仅支持2M以下的文件');
        return false;
      }
      // 如果通过了上述条件,允许上传
      return true;
    };
    
    const UserInfo: React.FC = () => {
      // 定义状态和钩子函数
      const [loading, setLoading] = useState(false);
      const [loginUser, setLoginUser] = useState<API.UserVO>();
      const [previewOpen, setPreviewOpen] = useState(false);
      const [previewTitle, setPreviewTitle] = useState('');
      const [previewImage, setPreviewImage] = useState('');
      const [fileList, setFileList] = useState<UploadFile[]>([]);
      const [voucherLoading, setVoucherLoading] = useState<boolean>(false);
      const [userVoucher, setUserVoucher] = useState<API.UserVoucherVO>();
      const handleCancel = () => setPreviewOpen(false);
    
      const updateuserName = async (value: string) => {
        const res = await updateUserUsingPost({ id: loginUser?.id, userName: value });
        if (res.code !== 0) {
          message.error('更新失败');
          return;
        }
        message.success('昵称更新成功');
        location.reload();
        return;
      };
    
      /**
       * 更新凭证
       */
      const updateVoucher = async () => {
        setVoucherLoading(true);
        const res = await updateVoucherUsingPost();
        if (res.data && res.code === 0) {
          setUserVoucher(res.data);
          setTimeout(() => {
            message.success(`凭证更新成功`);
            setVoucherLoading(false);
          }, 800);
        }
      };
    
      /**
       * 获取文件base64
       * @param file
       */
      const getBase64 = (file: RcFile): Promise<string> =>
        new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.readAsDataURL(file);
          reader.onload = () => resolve(reader.result as string);
          reader.onerror = (error) => reject(error);
        });
    
      /**
       * 预览文件
       */
      const handlePreview = async (file: UploadFile) => {
        if (!file.url && !file.preview) {
          file.preview = await getBase64(file.originFileObj as RcFile);
        }
        setPreviewImage(file.url || (file.preview as string));
        setPreviewOpen(true);
        setPreviewTitle(file.name || file.url!.substring(file.url!.lastIndexOf('-') + 1));
      };
    
      /**
       * 上传文件参数列表
       */
      const uploadProps: UploadProps = {
        name: 'file',
        withCredentials: true,
        action: async (file: RcFile) => {
          setLoading(true);
          // @ts-ignore
          const res = await uploadAvatarUsingPost({ file });
          if (res.code !== 0) {
            message.error('上传失败');
            return '';
          }
    
          const url = requestConfig.baseURL + 'api/file/downloadAvatar?name=' + res.data;
          if (loginUser) {
            loginUser.userAvatar = url;
            setLoginUser(loginUser);
          }
          const updateRes = await updateUserUsingPost({ id: loginUser?.id, userAvatar: url });
          if (updateRes.code !== 0) {
            message.error('更新失败');
            return '';
          }
          message.success('头像更新成功');
          location.reload();
          setLoading(false);
          return url;
        },
        onChange: async function ({ file, fileList: newFileList }) {
          const { response } = file;
          if (file.response && response.data) {
            const {
              data: { status, url },
            } = response;
            const updatedFileList = [...fileList];
            if (response.code !== 0 || status === 'error') {
              message.error(response.message);
              file.status = 'error';
              updatedFileList[0] = {
                // @ts-ignore
                uid: loginUser?.userAccount,
                // @ts-ignore
                name: loginUser?.userAvatar
                  ? loginUser?.userAvatar?.substring(loginUser?.userAvatar!.lastIndexOf('-') + 1)
                  : 'error',
                status: 'error',
                percent: 100,
              };
              setFileList(updatedFileList);
              return;
            }
            file.status = status;
            updatedFileList[0] = {
              // @ts-ignore
              uid: loginUser?.userAccount,
              // @ts-ignore
              name: loginUser?.userAvatar?.substring(loginUser?.userAvatar!.lastIndexOf('-') + 1),
              status: status,
              url: url,
              percent: 100,
            };
            setFileList(updatedFileList);
          } else {
            setFileList(newFileList);
          }
        },
        listType: 'picture-circle',
        onPreview: handlePreview,
        fileList: fileList,
        beforeUpload: beforeUpload,
        maxCount: 1,
        progress: {
          strokeColor: {
            '0%': '#108ee9',
            '100%': '#87d068',
          },
          strokeWidth: 3,
          format: (percent) => percent && `${parseFloat(percent.toFixed(2))}%`,
        },
      };
    
      /**
       * 加载数据
       */
      const loadData = async () => {
        setLoading(true);
        setVoucherLoading(true);
        try {
          const resLoginUser = await getLoginUserUsingGet({});
          const resVoucher = await getVoucherUsingGet({});
          if (resLoginUser.code !== 0) {
            message.error('加载用户信息失败');
            return;
          }
          if (resVoucher.code !== 0) {
            message.error('加载用户信息失败');
            return;
          }
          setLoginUser(resLoginUser.data);
          setUserVoucher(resVoucher.data);
        } catch (error: any) {
          message.error('请求失败,' + error.message);
        }
        setLoading(false);
        setVoucherLoading(false);
      };
    
      useEffect(() => {
        loadData();
      }, []);
    
      return (
        <PageContainer
          header={{
            breadcrumb: {},
          }}
        >
          <ProCard bordered direction="column">
            <ProCard
              loading={loading}
              bordered
              type="inner"
              title={<strong>基本信息</strong>}
            >
              <Descriptions column={1}>
                <Descriptions.Item>
                  <ImgCrop
                    rotationSlider
                    quality={1}
                    aspectSlider
                    maxZoom={4}
                    cropShape={'round'}
                    zoomSlider
                    showReset
                  >
                    <Upload {...uploadProps}>
                      {loginUser?.userAvatar ? (
                        <img src={loginUser?.userAvatar} alt="example" style={{ width: '100%' }} />
                      ) : (
                        <div>
                          <PlusOutlined />
                          <div style={{ marginTop: 8 }}>Upload</div>
                        </div>
                      )}
                    </Upload>
                  </ImgCrop>
                  <Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handleCancel}>
                    <img alt="example" style={{ width: '100%' }} src={previewImage} />
                  </Modal>
                </Descriptions.Item>
                <Descriptions.Item label="昵称">
                  <Paragraph
                    editable={{
                      icon: <EditOutlined />,
                      tooltip: '编辑',
                      onChange: (value) => {
                        updateuserName(value);
                      },
                    }}
                  >
                    {loginUser?.userName ? loginUser?.userName : '无名氏'}
                  </Paragraph>
                </Descriptions.Item>
                <Descriptions.Item label="id">
                  <Paragraph copyable={true}>{loginUser?.id}</Paragraph>
                </Descriptions.Item>
                <Descriptions.Item label="角色">
                  {loginUser && loginUser.userRole === 'user' ? (
                    <Paragraph copyable={true}>{userRoleList.user.text}</Paragraph>
                  ) : null}
                  {loginUser && loginUser.userRole === 'admin' ? (
                    <Paragraph copyable={true}>{userRoleList.admin.text}</Paragraph>
                  ) : null}
                </Descriptions.Item>
              </Descriptions>
            </ProCard>
            <br/>
            <ProCard
              loading={voucherLoading}
              bordered
              type="inner"
              title={<strong>Api密钥</strong>}
              extra={
                <Button loading={voucherLoading} onClick={updateVoucher}>
                  {userVoucher?.secretId && userVoucher?.secretKey ? '更新' : '生成'}密钥
                </Button>
              }
            >
              {userVoucher?.secretId && userVoucher?.secretKey ? (
                <Descriptions column={1}>
                  <Descriptions.Item label="AccessKey">
                    <Paragraph copyable={valueLength(userVoucher?.secretId)}>
                      {userVoucher?.secretId}
                    </Paragraph>
                  </Descriptions.Item>
                  <Descriptions.Item label="SecretKey">
                    <Paragraph copyable={valueLength(userVoucher?.secretKey)}>
                      {userVoucher?.secretKey}
                    </Paragraph>
                  </Descriptions.Item>
                </Descriptions>
              ) : (
                '暂无凭证,请先生成凭证'
              )}
            </ProCard>
            <br/>
            <ProCard
                type="inner"
                title={<strong>开发者 SDK(快速接入API接口)</strong>}
                bordered
            >
              <Button size={"large"}>
                <a target={"_blank"} href={"https://github.com/qimu666/qi-api-sdk"}
                   rel="noreferrer"><VerticalAlignBottomOutlined/> Java SDK</a>
              </Button>
            </ProCard>
    
          </ProCard>
    
        </PageContainer>
      );
    };
    
    export default UserInfo;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339

# 查看接口文档

  • 主要分为三个部分

    1. 接口基本信息
    2. 接口文档在线查看(后续开发一个纯前端的文档项目,可以使用hexo等)
    3. 接口操作页面
      1. 接口文档
      2. 在线调试工具
      3. 错误响应参数
      4. 代码示例(java/axios)
  • 代码

    • ListOpenApi/info/index.tsx

      import CodeHighlighting from '@/components/CodeHighlighting';
      import { interfaceMethodList, interfaceStatusList } from '@/enum/interfaceInfoEnum';
      import {
        axiosExample, javaExample,
        requestParameters,
        responseParameters,
        returnExample,
      } from '@/pages/ListOpenApi/info/components/defaultCode';
      import ToolsTab from '@/pages/ListOpenApi/info/components/ToolsTab';
      import {
        getInterfaceInfoVoByIdUsingGet,
        invokeInterfaceInfoUsingPost
      } from '@/services/gigotapi-backend/interfaceInfoController';
      import { useParams } from '@@/exports';
      import {
        BugOutlined,
        CodeOutlined,
        FileExclamationOutlined,
        FileTextOutlined,
      } from '@ant-design/icons';
      import { PageContainer, ProColumns } from '@ant-design/pro-components';
      import {Badge, Card, Descriptions, Form, message, Select, Table, Tabs} from 'antd';
      import { Column } from 'rc-table';
      import React, { useEffect, useState } from 'react';
      import './index.less';
      import {errorCode} from "@/enum/ErrorCodeEnum";
      
      const { Option } = Select;
      
      type toolsParams = {
        id: number;
        paramsName: string;
        paramsValue: string;
      };
      
      const ListOpenApiInfo: React.FC = () => {
        // 定义状态和钩子函数
        const [loading, setLoading] = useState(false);
        const [data, setData] = useState<API.InterfaceInfoVO>();
        const params = useParams();
        const [activeTabKey, setActiveTabKey] = useState<
          'tools' | 'api' | 'errorCode' | 'sampleCode' | string
        >('api');
        const [form] = Form.useForm();
        const [requestExampleActiveTabKey, setRequestExampleActiveTabKey] = useState<string>('javadoc');
        const [javaCode, setJavaCode] = useState<any>();
        const [axiosCode, setAxiosCode] = useState<any>();
      
      
      
        // 在线调试工具输入框提示
        const [toolsInputPlaceholderValue, setToolsInputPlaceholderValue] = useState<string>();
        // 在线调试工具输入框的值
        const [toolsInputValue, setToolsInputValue] = useState<string>();
        //
        const [toolsProEditTableData, setToolsProEditTableData] = useState<toolsParams[]>([]);
        const [toolsProEditTableDefaultData, setToolsProEditTableDefaultData] = useState<
          readonly toolsParams[]
        >([{ id: 1, paramsName: '名称1', paramsValue: '参数1' }]);
        const [toolsResultLoading, setToolsResultLoading] = useState(false);
        const [toolsResult, setToolsResult] = useState<string>();
      
      
      
        // 请求、响应参数
        const [requestParams, setRequestParams] = useState<string>();
        const [responseParams, setResponseParams] = useState<string>();
      
        /**
         * 返回状态码
         */
        const [returnCode, setReturnCode] = useState<any>(returnExample);
      
        // 解析出的地址
        const [exampleUrl, setExampleUrl] = useState<string>('');
        const [exampleParams, setExampleParams] = useState<
          {
            key: string;
            value: string;
          }[]
        >([]);
      
        const selectBefore = (
          <Select defaultValue={data?.method}>
            <Option value={interfaceMethodList.GET.text}>GET</Option>
            <Option value={interfaceMethodList.POST.text}>POST</Option>
            <Option value={interfaceMethodList.PUT.text}>PUT</Option>
            <Option value={interfaceMethodList.DELETE.text}>DELETE</Option>
          </Select>
        );
      
        // 在 toolsProEditTableData 中新增一行
        const handleProEditTableAdd = () => {
          alert('准备添加一行数据');
          const newData: toolsParams = {
            id: Date.now(),
            paramsName: '',
            paramsValue: '',
          };
          setToolsProEditTableData((prevData) => [...prevData, newData]);
        };
      
        const toolsInputDoubleClick = () => {
          if (toolsInputValue === '' || toolsInputValue === undefined) {
            setToolsInputValue(toolsInputPlaceholderValue);
            const params = exampleParams;
            const toolsProEditTableArr: toolsParams[] = [];
            for (let i = 0; i < params.length; i++) {
              toolsProEditTableArr.push({
                id: Date.now(),
                paramsName: params[i].key,
                paramsValue: params[i].value,
              });
              setToolsProEditTableData((prevData) => [...prevData, toolsProEditTableArr[i]]);
            }
          }
        };
        const toolsInputChange = (even) => {
          setToolsInputValue(even.target.value);
        };
      
        const requestExampleTabChange = (key: string) => {
          setRequestExampleActiveTabKey(key);
        };
      
        /**
         * 在线调试工具默认显示的输入框
         */
        const toolsInputPlaceholder = () => {
          let resPlaceholder = '';
          // 健壮性校验
          const method = data?.method;
          if (method === undefined) {
            setToolsInputPlaceholderValue(resPlaceholder);
            return;
          }
          const url: string | undefined = data?.requestExample;
          if (url === undefined || url === null) {
            setToolsInputPlaceholderValue(resPlaceholder);
            return;
          }
      
          // GET
          if (method === interfaceMethodList.GET.text) {
            // 获取url?以前的字符
            resPlaceholder = url;
            // 判断是否有参数
            if (url.indexOf('?') !== -1) {
              const urlBefore = url.substring(0, url.indexOf('?'));
              // 获取参数
              const params = getParams(url);
              setExampleParams(params);
      
              resPlaceholder = urlBefore;
            }
            setToolsInputPlaceholderValue(resPlaceholder);
            return;
          }
      
          // 其他请求
          resPlaceholder = url;
          setToolsInputPlaceholderValue(resPlaceholder);
          return;
        };
      
        /**
         * 获取params
         */
        const getParams = (url: string) => {
          const urlObj = new URL(url);
          const params = new URLSearchParams(urlObj.search);
          const result: {
            key: string;
            value: string;
          }[] = [];
      
          for (let [key, value] of params.entries()) {
            result.push({ key, value });
          }
      
          return result;
        };
      
        /**
         * 提交在线调试
         */
        const submitTools = async () => {
          // 校验地址和参数是否输入
          if (toolsInputValue === '') {
            message.error('请填写地址');
          }
          setToolsResultLoading(true);
          const paramsValuesArray = toolsProEditTableData.map(item => item.paramsValue);
          const res = await invokeInterfaceInfoUsingPost({ id: data?.id, requestParams: paramsValuesArray});
          setToolsResult(JSON.stringify(res, null, 4))
          setToolsResultLoading(false);
        };
      
        const toolsParamsColumns: ProColumns<toolsParams>[] = [
          {
            title: '参数名称',
            dataIndex: 'paramsName',
            valueType: 'text',
            search: false,
          },
          {
            title: '参数值',
            dataIndex: 'paramsValue',
            valueType: 'text',
            search: false,
          },
          {
            title: '操作',
            valueType: 'option',
            render: (text, record, _, action) => [
              <a
                key="editable"
                onClick={() => {
                  action?.startEditable?.(record.id);
                }}
              >
                编辑
              </a>,
              <a
                key="delete"
                onClick={() => {
                  setToolsProEditTableData(toolsProEditTableData.filter((item) => item.id !== record.id));
                }}
              >
                删除
              </a>,
            ],
          },
        ];
      
        /**
         *
         */
        const responseExampleContentList: Record<string, React.ReactNode> = {
          api: (
            <>
              <p className="highlightLine" style={{ marginTop: 15 }}>
                请求参数说明:<a onClick={() => setActiveTabKey('sampleCode')}>见示例代码</a>
              </p>
              <Table
                dataSource={requestParams && requestParams.length > 0 ? requestParams : requestParameters}
                pagination={false}
                style={{ maxWidth: 800 }}
                size={'small'}
              >
                <Column title="参数名称" dataIndex="fieldName" key="fieldName" />
                <Column title="必选" dataIndex="required" key="required" />
                <Column title="类型" dataIndex="type" key="type" />
                <Column title="描述" dataIndex="desc" key="desc" />
              </Table>
              <p className="highlightLine" style={{ marginTop: 15 }}>
                响应参数说明:<a onClick={() => setActiveTabKey('errorCode')}>错误码参照</a>
              </p>
              <Table
                dataSource={
                  responseParams && responseParams?.length > 0 ? responseParams : responseParameters
                }
                pagination={false}
                style={{ maxWidth: 800 }}
                size={'small'}
              >
                <Column title="参数名称" dataIndex="fieldName" key="fieldName" />
                <Column title="类型" dataIndex="type" key="type" />
                <Column title="描述" dataIndex="desc" key="desc" />
              </Table>
                <p className="highlightLine" style={{marginTop: 15}}>返回示例:</p>
                <CodeHighlighting codeString={returnCode} language={'javascript'}/>
            </>
          ),
          tools: (
            <>
              <ToolsTab
                toolsInputPlaceholderValue={toolsInputPlaceholderValue}
                toolsInputValue={toolsInputValue}
                toolsInputDoubleClick={toolsInputDoubleClick}
                toolsInputChange={toolsInputChange}
                submitTools={submitTools}
                toolsProEditTableDefaultData={toolsProEditTableDefaultData}
                toolsProEditTableData={toolsProEditTableData}
                setToolsProEditTableData={setToolsProEditTableData}
                handleProEditTableAdd={handleProEditTableAdd}
                toolsParamsColumns={toolsParamsColumns}
                data={data}
                toolsResultLoading={toolsResultLoading}
                toolsResult={toolsResult}
              />
            </>
          ),
          errorCode: (
              <>
                <p className="highlightLine">错误码:</p>
                <Table dataSource={errorCode} pagination={false} style={{maxWidth: 800}} size={"small"}>
                  <Column title="参数名称" dataIndex="name" key="name"/>
                  <Column title="错误码" dataIndex="code" key="code"/>
                  <Column title="描述" dataIndex="des" key="des"/>
                </Table>
              </>
          ),
          sampleCode: (
            <>
              <Tabs
                  defaultActiveKey="javadoc"
                  centered
                  onChange={requestExampleTabChange}
                  items={[
                    {
                      key: 'javadoc',
                      label: 'java',
                      children: <CodeHighlighting codeString={javaCode} language={"java"}/>
                    },
                    {
                      key: 'javascript',
                      label: 'axios',
                      children: <CodeHighlighting codeString={axiosCode} language={requestExampleActiveTabKey}/>
                    },
                  ]}
              />
            </>
          ),
        };
        /**
         * 修改tab状态
         * @param key
         */
        const responseExampleTabChange = (key: string) => {
          if (key === 'tools') {
            toolsInputPlaceholder();
          }
          setActiveTabKey(key);
        };
        /**
         *
         */
        const responseExampleTabList = [
          {
            key: 'api',
            label: (
              <>
                <FileTextOutlined />
                API文档
              </>
            ),
          },
          {
            key: 'tools',
            label: (
              <>
                <BugOutlined />
                在线调试工具
              </>
            ),
          },
          {
            key: 'errorCode',
            label: (
              <>
                <FileExclamationOutlined />
                错误码参照
              </>
            ),
          },
          {
            key: 'sampleCode',
            label: (
              <>
                <CodeOutlined />
                示例代码
              </>
            ),
          },
        ];
      
        const loadData = async () => {
          // 初始化数据
          if (!params.id) {
            message.error('参数不存在');
            return;
          }
          setLoading(true);
          try {
            const res = await getInterfaceInfoVoByIdUsingGet({
              id: params.id,
            });
            setData(res.data);
      
            // 获取请求参数和响应参数
            let requestParams = res.data.requestParams;
            let responseParams = res.data.responseParams;
            try {
              setRequestParams(requestParams ? JSON.parse(requestParams) : []);
              setResponseParams(responseParams ? JSON.parse(responseParams) : []);
            } catch (e: any) {
              setRequestParams([]);
              setResponseParams([]);
            }
            setAxiosCode(axiosExample(res.data?.url, res.data?.method?.toLowerCase()));
            setJavaCode(javaExample(res.data?.url, res.data?.method?.toUpperCase()));
          } catch (error: any) {
            message.error('请求失败,' + error.message);
          }
          setLoading(false);
        };
      
        useEffect(() => {
          loadData();
        }, [toolsProEditTableData]);
      
        return (
          <PageContainer
            header={{
              breadcrumb: {},
            }}
          >
            <Card>
              {data ? (
                <Descriptions
                  title={
                    <div>
                      <div><p className="highlightLine" style={{fontSize: 20}}>{data.name}</p></div>
                      <div style={{ fontSize: '12px', color: '#C0C0C0' }}>{data.description}</div>
                    </div>
                  }
                  column={3}
                >
                  <Descriptions.Item label="请求地址">{data.url}</Descriptions.Item>
                  <Descriptions.Item label="返回格式">{data.returnFormat}</Descriptions.Item>
                  <Descriptions.Item label="请求方式">{data.method}</Descriptions.Item>
                  <Descriptions.Item label="请求示例">{data.requestExample}</Descriptions.Item>
                  <Descriptions.Item label="花费金币">{data.payGoldCoin}个</Descriptions.Item>
                  <Descriptions.Item label="接口状态">
                    {data && data.status === 0 ? (
                      <Badge
                        status={interfaceStatusList['0'].status}
                        text={interfaceStatusList['0'].text}
                      />
                    ) : null}
                    {data && data.status === 1 ? (
                      <Badge
                        status={interfaceStatusList['1'].status}
                        text={interfaceStatusList['1'].text}
                      />
                    ) : null}
                  </Descriptions.Item>
                </Descriptions>
              ) : (
                <>接口不存在</>
              )}
            </Card>
            <Card>
              <p className="highlightLine">接口详细描述请前往开发者在线文档查看:</p>
              {/*<a href={`${docUrl}/pages/${data?.id}/#${data?.name}`} target={"_blank"} rel="noreferrer">📘*/}
              <a href={`132/pages/${data?.id}/#${data?.name}`} target={'_blank'} rel="noreferrer">
                📘 接口在线文档:{data?.name}
              </a>
            </Card>
            <br />
            <Card
              tabList={responseExampleTabList}
              activeTabKey={activeTabKey}
              onTabChange={responseExampleTabChange}
            >
              {responseExampleContentList[activeTabKey]}
            </Card>
          </PageContainer>
        );
      };
      
      export default ListOpenApiInfo;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      403
      404
      405
      406
      407
      408
      409
      410
      411
      412
      413
      414
      415
      416
      417
      418
      419
      420
      421
      422
      423
      424
      425
      426
      427
      428
      429
      430
      431
      432
      433
      434
      435
      436
      437
      438
      439
      440
      441
      442
      443
      444
      445
      446
      447
      448
      449
      450
      451
      452
      453
      454
      455
      456
      457
      458
      459
      460
      461
      462
      463
      464
      465
      466
      467
      468
      469
      470
      471
      472
      473
      474
    • ListOpenApi/info/index.less 样式

      .highlightLine:before {
        content: '';
        width: 4px;
        height: 16px;
        background: #00aeff;
        border-radius: 2px;
        position: absolute;
        left: 0;
        top: 0;
      }
      
      .highlightLine {
        font-size: 14px;
        font-weight: 700;
        color: #555;
        padding-left: 14px;
        height: 16px;
        line-height: 16px;
        margin-bottom: 18px;
        position: relative;
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
    • ListOpenApi/info/components/ToolsTab 在线调试工具封装组件

      import CodeHighlighting from '@/components/CodeHighlighting';
      import { interfaceMethodList } from '@/enum/interfaceInfoEnum';
      import { EditableProTable, ProForm } from '@ant-design/pro-components';
      import '@umijs/max';
      import {Button, Empty, Input, Select, Space, Spin, Tooltip} from 'antd';
      import React, {useState} from 'react';
      
      export type Props = {
        toolsInputPlaceholderValue: string;
        toolsInputValue: string;
        toolsInputDoubleClick: () => void;
        toolsInputChange: (e: any) => void;
        submitTools: () => void;
        toolsProEditTableDefaultData: any;
        toolsProEditTableData: any;
        setToolsProEditTableData: (data: any) => void;
        handleProEditTableAdd: (data: any) => void;
        requestParam: any;
        temporaryParams: any;
        toolsParamsColumns: any;
        data: any;
        toolsParams: any;
        toolsResult?: string;
        requestExampleActiveTabKey: string;
        toolsResultLoading: boolean;
      };
      const { Option } = Select;
      
      const ToolsTab: React.FC<Props> = (props) => {
        const {
          toolsInputPlaceholderValue,
          toolsInputValue,
          toolsInputDoubleClick,
          toolsInputChange,
          submitTools,
          toolsProEditTableDefaultData,
          toolsProEditTableData,
          setToolsProEditTableData,
          handleProEditTableAdd,
          toolsParamsColumns,
          data,
          toolsResult,
          requestExampleActiveTabKey,
          toolsResultLoading,
        } = props;
        const selectBefore = (
          <Select defaultValue={data?.method}>
            <Option value={interfaceMethodList.GET.text}>GET</Option>
            <Option value={interfaceMethodList.POST.text}>POST</Option>
            <Option value={interfaceMethodList.PUT.text}>PUT</Option>
            <Option value={interfaceMethodList.DELETE.text}>DELETE</Option>
          </Select>
        );
        return (
          <>
            <ProForm style={{ width: '100%' }} submitter={false}>
              <ProForm.Group style={{ width: '100%' }}>
                <ProForm.Item label={<p className="highlightLine"> 请求地址</p>} style={{ width: '100%' }}>
      
                  <Space.Compact style={{ width: '800px' }} onFocus={() =>{}} onBlur={() =>{}}>
      
                    <Input
                      size={'large'}
                      addonBefore={selectBefore}
                      placeholder={toolsInputPlaceholderValue}
                      value={toolsInputValue}
                      onDoubleClick={toolsInputDoubleClick}
                      onChange={toolsInputChange}
                    />
                    <Button name={'send'} type="primary" size={'large'} onClick={submitTools}>
                      发送
                    </Button>
                  </Space.Compact>
                </ProForm.Item>
                <ProForm.Item
                  label={<p className="highlightLine"> 请求参数</p>}
                  name="dataSource"
                  trigger="onValuesChange"
                >
                  <Button type={'default'} onClick={() => setToolsProEditTableData([])}>清空</Button>
                  <br/>
                  <EditableProTable<toolsParams>
                    rowKey="id"
                    toolBarRender={false}
                    columns={toolsParamsColumns}
                    defaultData={toolsProEditTableDefaultData}
                    value={toolsProEditTableData}
                    onChange={setToolsProEditTableData}
                    loading={false}
                    recordCreatorProps={{
                      newRecordType: 'dataSource',
                      position: 'bottom',
                      onRecordAdd: handleProEditTableAdd,
                      record: () => ({
                        id: (Math.random() * 1000000).toFixed(0),
                        addonBefore: 'ccccccc',
                        decs: 'testdesc',
                      }),
                    }}
                    editable={{
                      type: 'multiple',
                      actionRender: (row, _, dom) => {
                        return [dom.delete];
                      },
                    }}
                  />
                  <p className="highlightLine" style={{marginTop: 25}}>返回结果</p>
                  <Spin spinning={toolsResultLoading}>
                    {toolsResult ?
                        <CodeHighlighting codeString={toolsResult} language={requestExampleActiveTabKey}/>
                        : <Empty description={"未发起调用,暂无请求信息"}/>
                    }
                  </Spin>
                </ProForm.Item>
              </ProForm.Group>
            </ProForm>
          </>
        );
      };
      export default ToolsTab;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
    • ListOpenApi/info/components/ApiTab 文档

      import '@umijs/max';
      import React from 'react';
      import {Table} from "antd";
      import {Column} from "rc-table";
      import CodeHighlighting from '@/components/CodeHighlighting';
      import {requestParameters, responseParameters} from '../defaultCode';
      
      export type Props = {
          requestParams?: [];
          responseParams?: [];
          errorCodeTab: () => void;
          sampleCode: () => void;
          returnCode: string
      };
      const ApiTab: React.FC<Props> = (props) => {
          const {requestParams, errorCodeTab, sampleCode, responseParams, returnCode} = props;
      
          return <>
              <p className="highlightLine" style={{marginTop: 15}}>请求参数说明:</p>
              <Table dataSource={requestParams && requestParams.length > 0 ? requestParams : requestParameters}
                     pagination={false}
                     style={{maxWidth: 800}} size={"small"}>
                  <Column title="参数名称" dataIndex="fieldName" key="fieldName"/>
                  <Column title="必选" dataIndex="required" key="required"/>
                  <Column title="类型" dataIndex="type" key="type"/>
                  <Column title="描述" dataIndex="desc" key="desc"/>
              </Table>
              <p className="highlightLine" style={{marginTop: 15}}>响应参数说明:<a
                  onClick={() => errorCodeTab?.()}>错误码参照</a></p>
              <Table dataSource={responseParams && responseParams?.length > 0 ? responseParams : responseParameters}
                     pagination={false}
                     style={{maxWidth: 800}}
                     size={"small"}>
                  <Column title="参数名称" dataIndex="fieldName" key="fieldName"/>
                  <Column title="类型" dataIndex="type" key="type"/>
                  <Column title="描述" dataIndex="desc" key="desc"/>
              </Table>
              {/*<p className="highlightLine" style={{marginTop: 15}}>请求示例:</p>*/}
              <a onClick={() => sampleCode?.()}>见示例代码</a>
              <p className="highlightLine" style={{marginTop: 15}}>返回示例:</p>
              <CodeHighlighting codeString={returnCode} language={'javascript'}/>
          </>
      };
      export default ApiTab;
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
    • ListOpenApi/defaultCode 一些默认代码

      import React from "react";
      import {ProColumns} from "@ant-design/pro-components";
      
      /**
       * axios代码示例
       * @param url
       * @param method
       */
      export const axiosExample = (url?: string, method?: string) =>
          `axios.${method}('${url}')
          .then(response => {
            console.log(response.data);
          })
          .catch(error => {
            console.error('请求发生错误:', error);
          });`;
      export const javaExample = (url?: string, method?: string) =>
          `    @Resource
          private ApiService apiService;
      
          public Object request() {
              BaseResponse baseResponse;
              try {
                  CurrencyRequest currencyRequest = new CurrencyRequest();
                  currencyRequest.setPath("${url}");
                  currencyRequest.setMethod("${method}");
                  currencyRequest.setRequestParams("你的请求参数,详细请前往开发者在线文档📘查看");
                  baseResponse = apiService.request(currencyRequest);
                  System.out.println("data = " + baseResponse.getData());
              } catch (BusinessException e) {
                  log.error(e.getMessage());
              }
              return baseResponse.getData();
          }`;
      /**
       * 返回示例
       */
      export const returnExample = '{\n' +
          '    "code": 0,\n' +
          '    "data": {} ,\n' +
          '    "message": "ok"\n' +
          '}'
      
      export const responseParameters = [{
          fieldName: 'code',
          type: "int",
          desc: <>返回码</>,
          required: '是'
      }, {
          fieldName: 'massage',
          type: "string",
          desc: "返回码描述",
          required: '是'
      }, {
          fieldName: 'data',
          type: "Object",
          desc: "返回数据",
          required: '是'
      }]
      
      export const requestParameters = [{
          fieldName: '无',
          type: "string",
          desc: "无",
          required: '否'
      }]
      
      export const requestParam: ProColumns[] = [
          {
              title: '参数名称',
              dataIndex: 'fieldName',
              formItemProps: {
                  rules: [
                      {
                          required: true,
                          message: '此项是必填项',
                      },
                  ],
              },
          }, {
              title: '参数值',
              dataIndex: 'value',
              formItemProps: {
                  rules: [
                      {
                          required: true,
                          message: '此项是必填项',
                      },
                  ],
              },
          },]
      
      
      export const DEFAULT_ADD_FIELD = {
          "fieldName": "",
          "value": ''
      };
      
      
      
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101

# 充值服务

  1. 产品列表

    import React, { useEffect, useState } from 'react';
    import { Button, Card, message, Spin, Tooltip } from 'antd';
    import { history, useModel } from '@umijs/max';
    import { PayCircleOutlined } from '@ant-design/icons';
    import { listProductInfoVoByPageUsingPost } from '@/services/gigotapi-backend/productInfoController';
    import { CheckCard, ProCard } from '@ant-design/pro-components';
    import GoldCoin from "@/components/Icon/GoldCoin";
    
    const ProductInfo: React.FC = () => {
      const [loading, setLoading] = useState<boolean>(false);
      const [product, setProduct] = useState<API.ProductInfoVO[]>();
      const { initialState, setInitialState } = useModel('@@initialState');
      const { loginUser } = initialState || {};
      const [total, setTotal] = useState<any>('0.00');
      const [productId, setProductId] = useState<any>('');
    
      useEffect(() => {
        if (total === '0.00') {
          setProductId('');
        }
      }, [total]);
    
      const loadData = async () => {
        setLoading(true);
        const res = await listProductInfoVoByPageUsingPost({ current: "1", pageSize: "10" });
        if (res.data && res.code === 0) {
          setProduct(res.data.records || []);
          setLoading(false);
        }
      };
    
      useEffect(() => {
        loadData();
      }, []);
    
      // @ts-ignore
      return (
        <>
          <Spin spinning={loading}>
            <Card style={{ minWidth: 360 }}>
              <ProCard
                type={'inner'}
                headerBordered
                bordered
                tooltip={'用于平台接口调用'}
                title={<strong>我的钱包</strong>}
              >
                <strong>金币 : </strong>
                <span style={{ color: 'red', fontSize: 18 }}>🪙{loginUser?.balanceGoldCoin}️</span>
              </ProCard>
              <br />
              <Card type={'inner'} title={<strong>积分商城 🪙️</strong>}>
                <ProCard wrap>
                  <CheckCard.Group
                    onChange={(checkedValue) => {
                      if (!checkedValue) {
                        setTotal('0.00');
                        return;
                      }
                      setTotal(checkedValue);
                    }}
                  >
                    {product &&
    
                        product.slice().sort((a, b) => parseInt(a.total) - parseInt(b.total)).map((item) => (
                        <CheckCard
                          key={item.id}
                          onClick={() => {
                            setTotal(item.total);
                            setProductId(item.id);
                          }}
                          description={item.description}
                          extra={
                            <>
                              <h3
                                // @ts-ignore
                                style={{
                                  color: 'red',
                                  fontSize: item.productType === 'EXPERIENCE' ? 16 : 18,
                                  fontWeight: 'bold',
                                }}
                              >
                                ¥{item.productType === 'EXPERIENCE' ? '🔥体验 ' : null}
                                {/*// @ts-ignore*/}
                                {item?.total / 100}
                              </h3>
                            </>
                          }
                          // @ts-ignore
                          actions={
                            <>
                              <GoldCoin></GoldCoin>
                            </>
                          }
                          style={{ width: 220, height: 330 }}
                          title={<strong>🪙 {item.addGoldCoin} 金币</strong>}
                          value={item.total}
                        />
                      ))}
                  </CheckCard.Group>
                </ProCard>
                <br />
                <ProCard style={{ marginTop: -20 }} layout={'center'}>
                  <span>
                    本商品为虚拟内容,用于平台接口调用,购买后不支持
                    <strong style={{ color: 'red' }}>退换</strong>。确认支付表示您已阅读并接受
                    {/*todo 改为自己的用户协议*/}
                    <a
                      target={'_blank'}
                      href={
                        'https://gitee.com/qimu6/statement/blob/master/%E6%9F%92%E6%9C%A8%E6%8E%A5%E5%8F%A3%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.md#%E6%9F%92%E6%9C%A8%E6%8E%A5%E5%8F%A3%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE'
                      }
                      rel="noreferrer"
                    >
                      {' '}
                      用户协议{' '}
                    </a>
                    ,如付款成功后10分钟后未到账,请联系站长微信:
                    <Tooltip
                      placement="bottom"
                      title={<img src={"https://gigot-1315824716.cos.ap-chongqing.myqcloud.com/pictrue/wechatQRCode.jpg"} alt="微信 code_nav" width="120" />}
                    >
                      <a>PYW</a>
                    </Tooltip>
                  </span>
                </ProCard>
              </Card>
              <br />
              <ProCard bordered headerBordered>
                <div
                  style={{
                    display: 'flex',
                    justifyContent: 'flex-end',
                    alignItems: 'center',
                    alignContent: 'center',
                  }}
                >
                  <div style={{ marginRight: '12px', fontWeight: 'bold', fontSize: 18 }}>实付</div>
                  <div style={{ marginRight: '20px', fontWeight: 'bold', fontSize: 18, color: 'red' }}>
                    ¥ {total / 100} 元
                  </div>
                  <Button
                    style={{ width: 100, padding: 5 }}
                    onClick={() => {
                      if (!productId) {
                        message.error('请先选择积分规格哦');
                        return;
                      }
                      message.loading('正在前往收银台,请稍后.....', 0.6);
                      setTimeout(() => {
                        history.push(`/ProductInfo/pay/${productId}`);
                      }, 800);
                    }}
                    size={'large'}
                    type={'primary'}
                  >
                    立即购买
                  </Button>
                </div>
              </ProCard>
            </Card>
          </Spin>
        </>
      );
    };
    
    export default ProductInfo;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
  2. 支付页面

    import { Card, message, QRCode, Radio, Spin, Tooltip } from 'antd';
    import React, { useEffect, useState } from 'react';
    import { history } from '@umijs/max';
    import WxPay from '@/components/Icon/WxPay';
    import ProCard from '@ant-design/pro-card';
    import Alipay from '@/components/Icon/Alipay';
    import { useParams } from '@@/exports';
    import { valueLength } from '@/pages/User/info';
    import {payCreateProductOrderUsingPost} from "@/services/gigotapi-backend/productOrderController";
    
    const PayOrder: React.FC = () => {
      const [loading, setLoading] = useState<boolean>(false);
      const [order, setOrder] = useState<API.ProductOrderVO>();
      const [total, setTotal] = useState<any>('0.00');
      const [status, setStatus] = useState<string>('active');
      const [payType, setPayType] = useState<string>();
      const urlParams = new URL(window.location.href).searchParams;
      const codeUrl = urlParams.get('codeUrl');
      const urlPayType = urlParams.get('payType');
      const [qrCode, setQrCode] = useState<any>('暂未选择支付方式');
      const params = useParams();
      const createOrder = async () => {
        setLoading(true);
        setStatus('loading');
        // @ts-ignore
        const res = await payCreateProductOrderUsingPost({productId: params.id, payType: payType });
        if (res.code === 0 && res.data) {
          setOrder(res.data);
          // @ts-ignore
          setTotal(res.data.total / 100);
          setStatus('active');
          setLoading(false);
          setQrCode(res.data.codeUrl);
        }
        if (res.code === 50001) {
          history.back();
        }
      };
      const queryOrderStatus = async () => {
        const currentTime = new Date();
        const expirationTime = new Date(order?.expirationTime as any);
        if (currentTime > expirationTime) {
          setStatus('expired');
        }
        return await queryOrderStatusUsingPOST({ orderNo: order?.orderNo });
      };
    
      const toAlipay = async () => {
        if (!params.id) {
          message.error('参数不存在');
          return;
        }
        setLoading(true);
        const res = await createOrderUsingPOST({ productId: params.id, payType: 'ALIPAY' });
        if (res.code === 0 && res.data) {
          message.loading('正在前往收银台,请稍后....');
          setTimeout(() => {
            document.write(res?.data?.formData as string);
            setLoading(false);
          }, 2000);
        } else {
          setLoading(false);
        }
      };
      const changePayType = (value: string) => {
        setPayType(value);
      };
      useEffect(() => {
        if (!params.id) {
          message.error('参数不存在');
          return;
        }
        // 判断是否为手机设备
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
          navigator.userAgent,
        );
        if (codeUrl) {
          if (isMobile) {
            window.location.href = codeUrl;
            return;
          }
          setQrCode(codeUrl);
          setStatus('active');
          setPayType('WX');
          return;
        }
        if (!urlPayType && !payType) {
          message.error('请选择支付方式');
          setStatus('expired');
          return;
        }
        if (urlPayType) {
          setPayType(urlPayType);
          return;
        }
        createOrder();
      }, []);
    
      useEffect(() => {
        if (payType === 'ALIPAY') {
          toAlipay();
        }
        if (payType === 'WX' && !codeUrl) {
          createOrder();
        }
      }, [payType]);
    
      useEffect(() => {
        if (order && order.orderNo && order.codeUrl) {
          const intervalId = setInterval(async () => {
            // 定时任务逻辑
            const res = await queryOrderStatus();
            if (res.data && res.code === 0) {
              setLoading(true);
              message.loading('支付成功,打款中....');
              clearInterval(intervalId);
              setTimeout(function () {
                setLoading(false);
                const urlParams = new URL(window.location.href).searchParams;
                history.push(urlParams.get('redirect') || '/account/center');
              }, 2000);
            } else {
              console.log('支付中...');
            }
          }, 3000);
          if (status === 'expired') {
            clearInterval(intervalId);
          }
          return () => {
            clearInterval(intervalId);
          };
        }
      }, [order, status]);
    
      return (
        <>
          <Card style={{ minWidth: 385 }}>
            <Spin spinning={loading}>
              <Card title={<strong>商品信息</strong>}>
                <div style={{ marginLeft: 10 }}>
                  <h3>{order?.productInfo?.name}</h3>
                  <h4>
                    {valueLength(order?.productInfo?.description)
                      ? order?.productInfo?.description
                      : '暂无商品描述信息'}
                  </h4>
                </div>
              </Card>
              <br />
              <ProCard bordered headerBordered layout={'center'} title={<strong>支付方式</strong>}>
                <Radio.Group name="payType" value={payType}>
                  <ProCard wrap gutter={18}>
                    <ProCard
                      onClick={() => {
                        changePayType('WX');
                      }}
                      hoverable
                      style={{
                        border:
                          payType === 'WX' ? '1px solid #1890ff' : '1px solid rgba(128, 128, 128, 0.5)',
                        maxWidth: 260,
                        minWidth: 210,
                        margin: 10,
                      }}
                      colSpan={{
                        xs: 24,
                        sm: 12,
                        md: 12,
                        lg: 12,
                        xl: 12,
                      }}
                    >
                      <Radio value={'WX'} style={{ fontSize: '1.12rem' }}>
                        <WxPay /> 微信支付
                      </Radio>
                    </ProCard>
                    <ProCard
                      onClick={() => {
                        changePayType('ALIPAY');
                      }}
                      hoverable
                      style={{
                        margin: 10,
                        maxWidth: 260,
                        minWidth: 210,
                        border:
                          payType === 'ALIPAY'
                            ? '1px solid #1890ff'
                            : '1px solid rgba(128, 128, 128, 0.5)',
                      }}
                      colSpan={{
                        xs: 24,
                        sm: 12,
                        md: 12,
                        lg: 12,
                        xl: 12,
                      }}
                    >
                      <Radio value={'ALIPAY'} style={{ fontSize: '1.2rem' }}>
                        <Alipay /> 支付宝
                      </Radio>
                    </ProCard>
                  </ProCard>
                </Radio.Group>
              </ProCard>
              <br />
              <Card title={'支付二维码'}>
                <br />
                <ProCard style={{ marginTop: -30 }} layout={'center'}>
                  <QRCode
                    errorLevel="H"
                    size={240}
                    value={qrCode}
                    // @ts-ignore
                    status={status}
                    onRefresh={() => {
                      if (!payType) {
                        message.error('请先选择支付方式');
                        return;
                      }
                      createOrder();
                    }}
                  />
                </ProCard>
                <ProCard
                  style={{
                    marginTop: -30,
                    color: '#f55f4e',
                    fontSize: 22,
                    display: 'flex',
                    fontWeight: 'bold',
                  }}
                  layout={'center'}
                >
                  ¥{total}
                </ProCard>
                <ProCard style={{ marginTop: -20 }} layout={'center'}>
                  <span>
                    本商品为虚拟内容,购买后不支持<strong style={{ color: 'red' }}>退换</strong>
                    。确认支付表示您已阅读并接受
                    {/*// todo 修改用户协议*/}
                    <a
                      target={'_blank'}
                      href={
                        'https://gitee.com/qimu6/statement/blob/master/%E6%9F%92%E6%9C%A8%E6%8E%A5%E5%8F%A3%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.md#%E6%9F%92%E6%9C%A8%E6%8E%A5%E5%8F%A3%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE'
                      }
                      rel="noreferrer"
                    >
                      {' '}
                      用户协议{' '}
                    </a>
                    如付款成功后10分钟后未到账,请联系站长微信:
                    <Tooltip
                      placement="bottom"
                      title={
                        <img
                          src={
                            'https://gigot-1315824716.cos.ap-chongqing.myqcloud.com/pictrue/wechatQRCode.jpg'
                          }
                          alt="微信 code_nav"
                          width="120"
                        />
                      }
                    >
                      <a>PYW</a>
                    </Tooltip>
                  </span>
                </ProCard>
              </Card>
            </Spin>
          </Card>
        </>
      );
    };
    
    export default PayOrder;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277

# 我的订单

  1. 订单列表

    import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
    import { PageContainer, ProDescriptions, ProTable } from '@ant-design/pro-components';
    import '@umijs/max';
    import { Drawer, message, Popconfirm } from 'antd';
    import { SortOrder } from 'antd/lib/table/interface';
    import React, { useRef, useState } from 'react';
    import { productPayTypeList, productStatusList } from '@/enum/ProductOrderEnum';
    import {
      deleteByIdProductOrderUsingPost,
      listMyProductOrderVoByPageUsingPost,
    } from '@/services/gigotapi-backend/productOrderController';
    import { Link } from '@umijs/max';
    const ProductOrder: React.FC = () => {
      /**
       * @en-US Pop-up window of new window
       * @zh-CN 新建窗口的弹窗
       *  */
      const [createModalVisiable, handleModalOpen] = useState<boolean>(false);
      /**
       * @en-US The pop-up window of the distribution update window
       * @zh-CN 分布更新窗口的弹窗
       * */
      const [updateModalVisible, handleUpdateModalOpen] = useState<boolean>(false);
      const [total, setTotal] = useState<number>();
    
      const [showDetail, setShowDetail] = useState<boolean>(false);
      const actionRef = useRef<ActionType>();
      const [currentRow, setCurrentRow] = useState<API.ProductOrderVO>();
      const [selectedRowsState, setSelectedRows] = useState<API.ProductOrderVO[]>([]);
    
      /**
       *  Delete node
       * @zh-CN 删除用户
       *
       * @param selectedRows
       */
      const handleRemove = async (selectedRows: API.DeleteRequest) => {
        const hide = message.loading('正在删除...');
        if (!selectedRows) return true;
        try {
          const currentId = currentRow?.id;
          await deleteByIdProductOrderUsingPost({ id: currentId });
          hide();
          message.success('删除成功');
          actionRef.current.reload();
          return true;
        } catch (error) {
          hide();
          return false;
        }
      };
    
      const deleteConfirm = async () => {
        await handleRemove(currentRow as API.DeleteRequest);
      };
    
      // @ts-ignore
      /**
       * @en-US International configuration
       * @zh-CN 国际化配置
       * */
      const columns: ProColumns<API.ProductOrderVO>[] = [
        {
          title: 'id',
          dataIndex: 'id',
          valueType: 'text',
          hideInTable: true,
          hideInForm: true,
          search: false,
        },
        {
          title: '订单名称',
          dataIndex: 'orderName',
          ellipsis: true,
          copyable: true,
          width: 120,
          render: (_, record) => (
            <Link key={record.id} to={`/ProductOrder/info/${record.id}`}>
              {record.orderName}
            </Link>
          ),
        },
        {
          title: '订单号',
          dataIndex: 'orderNo',
          valueType: 'text',
          formItemProps: {
            rules: [
              {
                required: true,
              },
            ],
          },
        },
    
        {
          title: '二维码地址',
          dataIndex: 'codeUrl',
          valueType: 'text',
          hideInTable: true,
          hideInForm: true,
          search: false,
        },
        {
          title: '创建人',
          dataIndex: 'userId',
          valueType: 'text',
          hideInTable: true,
          hideInForm: true,
          search: false,
        },
        {
          title: '商品id',
          dataIndex: 'productId',
          valueType: 'text',
          hideInTable: true,
          hideInForm: true,
          search: false,
        },
    
        {
          title: '金额',
          dataIndex: 'total',
          valueType: 'text',
          render: (text, record) => {
            return  '¥ ' + parseFloat(text) / 100 ;
          },
        },
        {
          title: '状态',
          dataIndex: 'status',
          valueEnum: productStatusList,
        },
        {
          title: '支付方式',
          dataIndex: 'payType',
          valueEnum: productPayTypeList,
        },
        {
          title: '商品信息',
          dataIndex: 'productInfo',
          valueType: 'text',
        },
        {
          title: '支付宝formData',
          dataIndex: 'formData',
          valueType: 'text',
          hideInTable: true,
          hideInForm: true,
          search: false,
        },
        {
          title: '增加金币个数',
          dataIndex: 'addGoldCoin',
          valueType: 'text',
          render: (text, record) => {
            return  '🪙 ' + text ;
          },
        },
        {
          title: '过期时间',
          dataIndex: 'expirationTime',
          valueType: 'dateTime',
          search: false,
        },
        {
          title: '创建时间',
          dataIndex: 'createTime',
          valueType: 'dateTime',
          search: false,
        },
        {
          title: '操作',
          dataIndex: 'option',
          valueType: 'option',
          render: (_, record) => [
            record.status === '1' ? (
              <a
                onClick={() => {
                  // todo 付款
                  setCurrentRow(record);
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                付款
              </a>
            ) : (
              <a
                onClick={() => {
                  // todo 取消订单
                  setCurrentRow(record);
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                取消订单
              </a>
            ),
            <Popconfirm
              key={'Delete'}
              title="请确认是否删除该订单!"
              onConfirm={deleteConfirm}
              okText="Yes"
              cancelText="No"
            >
              <a
                key="SUCCESS"
                style={{ color: 'red' }}
                onClick={async () => {
                  setCurrentRow(record);
                }}
              >
                删除
              </a>
            </Popconfirm>,
          ],
        },
      ];
      return (
        <PageContainer>
          <ProTable<API.RuleListItem, API.PageParams>
            headerTitle={''}
            actionRef={actionRef}
            rowKey="id"
            search={{
              labelWidth: 120,
            }}
            toolBarRender={() => []}
            /*
                                                                                              分页查询后端数据,这个地方是一个组件的托管,不管是刷新还是点击查询都是调用这个
                                                                                              这儿我们直接调用的返回参数和他不一致,因此需要重写返回参数
                                                                                            */
            request={async (
              params,
              sort: Record<string, SortOrder>,
              filter: Record<string, React.ReactText[] | null>,
            ) => {
              const res: any = await listMyProductOrderVoByPageUsingPost({
                ...params,
              });
              if (res?.data) {
                return {
                  data: res?.data.records || [],
                  success: true,
                  total: res?.data.total || 0,
                };
              } else {
                return {
                  data: [],
                  success: false,
                  total: 0,
                };
              }
            }}
            columns={columns}
            rowSelection={{
              onChange: (_, selectedRows) => {
                setSelectedRows(selectedRows);
              },
            }}
          />
    
          {/*{selectedRowsState?.length > 0 && (*/}
          {/*  <FooterToolbar*/}
          {/*    extra={*/}
          {/*      <div>*/}
          {/*        已选择{' '}*/}
          {/*        <a*/}
          {/*          style={{*/}
          {/*            fontWeight: 600,*/}
          {/*          }}*/}
          {/*        >*/}
          {/*          {selectedRowsState.length}*/}
          {/*        </a>{' '}*/}
          {/*        项 &nbsp;&nbsp;*/}
          {/*        <span>*/}
          {/*          服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)} 万*/}
          {/*        </span>*/}
          {/*      </div>*/}
          {/*    }*/}
          {/*  >*/}
          {/*    <Button*/}
          {/*      onClick={async () => {*/}
          {/*        await handleRemove(selectedRowsState);*/}
          {/*        setSelectedRows([]);*/}
          {/*        actionRef.current?.reloadAndRest?.();*/}
          {/*      }}*/}
          {/*    >*/}
          {/*      批量删除*/}
          {/*    </Button>*/}
          {/*  </FooterToolbar>*/}
          {/*)}*/}
    
          <Drawer
            width={600}
            open={showDetail}
            onClose={() => {
              setCurrentRow(undefined);
              setShowDetail(false);
            }}
            closable={false}
          >
            {currentRow?.name && (
              <ProDescriptions<API.RuleListItem>
                column={2}
                title={currentRow?.name}
                request={async () => ({
                  data: currentRow || {},
                })}
                params={{
                  id: currentRow?.name,
                }}
                columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
              />
            )}
          </Drawer>
        </PageContainer>
      );
    };
    export default ProductOrder;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
  2. 订单详情

    import CodeHighlighting from '@/components/CodeHighlighting';
    import { interfaceMethodList, interfaceStatusList } from '@/enum/interfaceInfoEnum';
    import {
      axiosExample, javaExample,
      requestParameters,
      responseParameters,
      returnExample,
    } from '@/pages/ListOpenApi/info/components/defaultCode';
    import ToolsTab from '@/pages/ListOpenApi/info/components/ToolsTab';
    import {
      getInterfaceInfoVoByIdUsingGet,
      invokeInterfaceInfoUsingPost
    } from '@/services/gigotapi-backend/interfaceInfoController';
    import { useParams } from '@@/exports';
    import {
      BugOutlined,
      CodeOutlined,
      FileExclamationOutlined,
      FileTextOutlined,
    } from '@ant-design/icons';
    import { PageContainer, ProColumns } from '@ant-design/pro-components';
    import {Badge, Card, Descriptions, Form, message, Select, Table, Tabs} from 'antd';
    import { Column } from 'rc-table';
    import React, { useEffect, useState } from 'react';
    import './index.less';
    import {errorCode} from "@/enum/ErrorCodeEnum";
    import {getProductOrderVoByIdUsingGet} from "@/services/gigotapi-backend/productOrderController";
    import {productPayTypeList, productStatusList} from "@/enum/ProductOrderEnum";
    
    const { Option } = Select;
    
    const ProductOrderInfo: React.FC = () => {
      // 定义状态和钩子函数
      const [loading, setLoading] = useState(false);
      const [data, setData] = useState<API.ProductOrderVO>();
      const params = useParams();
      const [activeTabKey, setActiveTabKey] = useState<
        'tools' | 'api' | 'errorCode' | 'sampleCode' | string
      >('api');
      const [form] = Form.useForm();
      const [requestExampleActiveTabKey, setRequestExampleActiveTabKey] = useState<string>('javadoc');
    
    
      const loadData = async () => {
        // 初始化数据
        if (!params.id) {
          message.error('参数不存在');
          return;
        }
        setLoading(true);
        try {
          const res = await getProductOrderVoByIdUsingGet({
            id: params.id,
          });
          setData(res.data);
          // 获取请求参数和响应参数
        } catch (error: any) {
          message.error('请求失败,' + error.message);
        }
        setLoading(false);
      };
    
      useEffect(() => {
        loadData();
      }, []);
    
      return (
        <PageContainer
          header={{
            breadcrumb: {},
          }}
        >
          <Card>
            {data ? (
              <Descriptions
                title={"基本信息"}
    
                column={2}
              >
                <Descriptions.Item label="订单名称">{data.orderName}</Descriptions.Item>
                <Descriptions.Item label="订单号">{data.orderNo}</Descriptions.Item>
                <Descriptions.Item label="商品描述">{data.productInfo?.description}</Descriptions.Item>
                <Descriptions.Item label="金额">¥ {data.total} 元</Descriptions.Item>
                <Descriptions.Item label="增加金币">{data.addGoldCoin}个</Descriptions.Item>
                <Descriptions.Item label="订单状态">
                  {data && data.status === '1' ? (
                    <Badge
                      status={productStatusList['1'].status}
                      text={productStatusList['1'].text}
                    />
                  ) : null}
                  {data && data.status === '2' ? (
                    <Badge
                      status={productStatusList['2'].status}
                      text={productStatusList['2'].text}
                    />
                  ) : null}
                  {data && data.status === '3' ? (
                    <Badge
                      status={productStatusList['3'].status}
                      text={productStatusList['3'].text}
                    />
                  ) : null}
                </Descriptions.Item>
                <Descriptions.Item label="支付方式">
    
                  {data && data.payType === 'wx' ? (
                    <>
                      {productPayTypeList['wx'].text}
                    </>
    
                  ) : null}
                  {data && data.payType === 'zfb' ? (
                    <>
                      {productPayTypeList['zfb'].text}
                    </>
                  ) : null}
                </Descriptions.Item>
                <Descriptions.Item label="过期时间">{data.expirationTime}</Descriptions.Item>
                <Descriptions.Item label="创建时间">{data.createTime}</Descriptions.Item>
              </Descriptions>
            ) : (
              <>接口不存在</>
            )}
          </Card>
        </PageContainer>
      );
    };
    
    export default ProductOrderInfo;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131

# 开发者文档

这是一个新项目,使用vuepress的vdoing主题,构建一个和ant design pro比较类似的文档查询网页 源码:开发者文档 (opens new window)

在线网址:http://doc.panyuwen.top/

image-20240305095944956

# 项目开发记录(后台管理)

# 接口管理

  • 模块所有的增删改查都在这儿~~~

    • Controller

      package top.panyuwen.gigotapi.controller;
      
      import cn.hutool.core.collection.CollUtil;
      import cn.hutool.core.collection.ListUtil;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.BeanUtils;
      import org.springframework.web.bind.annotation.*;
      import top.panyuwen.gigotapi.annotation.AuthCheck;
      import top.panyuwen.gigotapi.common.*;
      import top.panyuwen.gigotapi.constant.UserConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.InterfaceInfoAddRequest;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.InterfaceInfoEditRequest;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.InterfaceInfoQueryRequest;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.InterfaceInfoUpdateRequest;
      import top.panyuwen.gigotapi.model.entity.InterfaceInfo;
      import top.panyuwen.gigotapi.model.entity.User;
      import top.panyuwen.gigotapi.model.vo.InterfaceInfoVO;
      import top.panyuwen.gigotapi.service.InterfaceInfoService;
      import top.panyuwen.gigotapi.service.UserService;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.util.Collection;
      import java.util.HashSet;
      import java.util.List;
      import java.util.Set;
      import java.util.stream.Collectors;
      
      /**
       * 开放Api接口
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @RestController
      @RequestMapping("/interfaceInfo")
      @Slf4j
      public class InterfaceInfoController {
      
          @Resource
          private InterfaceInfoService interfaceInfoService;
      
          @Resource
          private UserService userService;
      
          // region 增删改查
      
          /**
           * 创建
           *
           * @param interfaceInfoAddRequest
           * @param request
           * @return
           */
          @PostMapping("/add")
          public BaseResponse<Long> addInterfaceInfo(@RequestBody InterfaceInfoAddRequest interfaceInfoAddRequest, HttpServletRequest request) {
              if (interfaceInfoAddRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              InterfaceInfo interfaceInfo = new InterfaceInfo();
              BeanUtils.copyProperties(interfaceInfoAddRequest, interfaceInfo);
              interfaceInfoService.validInterfaceInfo(interfaceInfo, true);
              User loginUser = userService.getLoginUser(request);
              interfaceInfo.setUserId(loginUser.getId());
              boolean result = interfaceInfoService.save(interfaceInfo);
              ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
              long newInterfaceInfoId = interfaceInfo.getId();
              return ResultUtils.success(newInterfaceInfoId);
          }
      
          /**
           * 删除
           *
           * @param deleteRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteById")
          public BaseResponse<Boolean> deleteByIdInterfaceInfo(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
              if (deleteRequest == null || deleteRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              long id = deleteRequest.getId();
              // 判断是否存在
              InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
              ThrowUtils.throwIf(oldInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可删除
              if (!oldInterfaceInfo.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean b = interfaceInfoService.removeById(id);
              return ResultUtils.success(b);
          }
      
          /**
           * 批量删除
           *
           * @param deleteListRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteByIds")
          public BaseResponse<Boolean> deleteByIdsInterfaceInfo(@RequestBody DeleteListRequest deleteListRequest, HttpServletRequest request) {
              if (deleteListRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              List<Long> ids = deleteListRequest.getIds();
              // 判断是否存在
              List<InterfaceInfo> interfaceInfos = interfaceInfoService.listByIds(ids);
              if(interfaceInfos.size() != ids.size()){
                  List<Long> idsInDB = interfaceInfos.stream().map(InterfaceInfo::getId).collect(Collectors.toList());
                  List<Long> differentIds = CollUtil.disjunction(ids, idsInDB).stream().collect(Collectors.toList());
                  log.error("differentIds:{}",differentIds);
                  ThrowUtils.throwIf(differentIds.size() > 0, ErrorCode.NOT_FOUND_ERROR,"未找到删除数据:differentIds:" + differentIds);
              }
              // 校验删除人是本人或者管理员
              List<Long> createUserIds = interfaceInfos.stream().map(InterfaceInfo::getUserId).distinct().collect(Collectors.toList());
              boolean isNotSelfAndAdmin = (createUserIds.size() > 1 || ! createUserIds.get(0).equals(user.getId())) && !userService.isAdmin(request);
              if (isNotSelfAndAdmin) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR,"只允许管理员或者本人删除");
              }
              boolean b = interfaceInfoService.removeByIds(ids);
              return ResultUtils.success(b);
          }
      
          /**
           * 更新(仅管理员)
           *
           * @param interfaceInfoUpdateRequest
           * @return
           */
          @PostMapping("/update")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> updateInterfaceInfo(@RequestBody InterfaceInfoUpdateRequest interfaceInfoUpdateRequest) {
              if (interfaceInfoUpdateRequest == null || interfaceInfoUpdateRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              InterfaceInfo interfaceInfo = new InterfaceInfo();
              BeanUtils.copyProperties(interfaceInfoUpdateRequest, interfaceInfo);
              // 参数校验
              interfaceInfoService.validInterfaceInfo(interfaceInfo, false);
              long id = interfaceInfoUpdateRequest.getId();
              // 判断是否存在
              InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
              ThrowUtils.throwIf(oldInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);
              boolean result = interfaceInfoService.updateById(interfaceInfo);
              return ResultUtils.success(result);
          }
      
          /**
           * 根据 id 获取
           *
           * @param id
           * @return
           */
          @GetMapping("/get/vo")
          public BaseResponse<InterfaceInfoVO> getInterfaceInfoVOById(long id, HttpServletRequest request) {
              if (id <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              InterfaceInfo interfaceInfo = interfaceInfoService.getById(id);
              if (interfaceInfo == null) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
              }
              return ResultUtils.success(interfaceInfoService.getInterfaceInfoVO(interfaceInfo, request));
          }
      
          /**
           * 分页获取列表(封装类)
           *
           * @param interfaceInfoQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/list/page/vo")
          public BaseResponse<Page<InterfaceInfoVO>> listInterfaceInfoVOByPage(@RequestBody InterfaceInfoQueryRequest interfaceInfoQueryRequest,
                  HttpServletRequest request) {
              long current = interfaceInfoQueryRequest.getCurrent();
              long size = interfaceInfoQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              Page<InterfaceInfo> interfaceInfoPage = interfaceInfoService.page(new Page<>(current, size),
                      interfaceInfoService.getQueryWrapper(interfaceInfoQueryRequest));
              return ResultUtils.success(interfaceInfoService.getInterfaceInfoVOPage(interfaceInfoPage, request));
          }
      
          /**
           * 分页获取当前用户创建的资源列表
           *
           * @param interfaceInfoQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/my/list/page/vo")
          public BaseResponse<Page<InterfaceInfoVO>> listMyInterfaceInfoVOByPage(@RequestBody InterfaceInfoQueryRequest interfaceInfoQueryRequest,
                  HttpServletRequest request) {
              if (interfaceInfoQueryRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User loginUser = userService.getLoginUser(request);
              interfaceInfoQueryRequest.setUserId(loginUser.getId());
              long current = interfaceInfoQueryRequest.getCurrent();
              long size = interfaceInfoQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              Page<InterfaceInfo> interfaceInfoPage = interfaceInfoService.page(new Page<>(current, size),
                      interfaceInfoService.getQueryWrapper(interfaceInfoQueryRequest));
              return ResultUtils.success(interfaceInfoService.getInterfaceInfoVOPage(interfaceInfoPage, request));
          }
      
          // endregion
      
      //    /**
      //     * 分页搜索(从 ES 查询,封装类)
      //     *
      //     * @param interfaceInfoQueryRequest
      //     * @param request
      //     * @return
      //     */
      //    @PostMapping("/search/page/vo")
      //    public BaseResponse<Page<InterfaceInfoVO>> searchInterfaceInfoVOByPage(@RequestBody InterfaceInfoQueryRequest interfaceInfoQueryRequest,
      //            HttpServletRequest request) {
      //        long size = interfaceInfoQueryRequest.getPageSize();
      //        // 限制爬虫
      //        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
      //        Page<InterfaceInfo> interfaceInfoPage = interfaceInfoService.searchFromEs(interfaceInfoQueryRequest);
      //        return ResultUtils.success(interfaceInfoService.getInterfaceInfoVOPage(interfaceInfoPage, request));
      //    }
      
          /**
           * 编辑(用户)
           *
           * @param interfaceInfoEditRequest
           * @param request
           * @return
           */
          @PostMapping("/edit")
          public BaseResponse<Boolean> editInterfaceInfo(@RequestBody InterfaceInfoEditRequest interfaceInfoEditRequest, HttpServletRequest request) {
              if (interfaceInfoEditRequest == null || interfaceInfoEditRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              InterfaceInfo interfaceInfo = new InterfaceInfo();
              BeanUtils.copyProperties(interfaceInfoEditRequest, interfaceInfo);
              // 参数校验
              interfaceInfoService.validInterfaceInfo(interfaceInfo, false);
              User loginUser = userService.getLoginUser(request);
              long id = interfaceInfoEditRequest.getId();
              // 判断是否存在
              InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
              ThrowUtils.throwIf(oldInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可编辑
              if (!oldInterfaceInfo.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean result = interfaceInfoService.updateById(interfaceInfo);
              return ResultUtils.success(result);
          }
      
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
    • ServiceImpl

      package top.panyuwen.gigotapi.service.impl;
      
      import cn.hutool.core.bean.BeanUtil;
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
      import com.google.gson.Gson;
      import org.apache.commons.collections4.CollectionUtils;
      import org.apache.commons.lang3.ObjectUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
      import org.springframework.stereotype.Service;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.constant.CommonConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.mapper.InterfaceInfoMapper;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.InterfaceInfoQueryRequest;
      import top.panyuwen.gigotapi.model.entity.InterfaceInfo;
      import top.panyuwen.gigotapi.model.entity.User;
      import top.panyuwen.gigotapi.model.vo.InterfaceInfoVO;
      import top.panyuwen.gigotapi.service.InterfaceInfoService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.SqlUtils;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.util.*;
      import java.util.stream.Collectors;
      
      /**
       * @author PYW
       * @description 针对表【interface_info(接口信息)】的数据库操作Service实现
       * @createDate 2023-12-05 17:11:19
       */
      @Service
      public class InterfaceInfoServiceImpl extends ServiceImpl<InterfaceInfoMapper, InterfaceInfo>
              implements InterfaceInfoService {
      
          private final static Gson GSON = new Gson();
      
          @Resource
          private UserService userService;
      
          @Resource
          private ElasticsearchRestTemplate elasticsearchRestTemplate;
      
          /**
           * 非空判断
           *
           * @param interfaceInfo
           * @param add
           */
          @Override
          public void validInterfaceInfo(InterfaceInfo interfaceInfo, boolean add) {
              if (interfaceInfo == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              String name = interfaceInfo.getName();
              String description = interfaceInfo.getDescription();
              String url = interfaceInfo.getUrl();
              String method = interfaceInfo.getMethod();
              // 创建时,参数不能为空
              if (add) {
                  ThrowUtils.throwIf(StringUtils.isAnyBlank(
                          name, url, method),
                          ErrorCode.PARAMS_ERROR,"名称,地址,请求方式必填!");
              }
              // 有参数则校验
              if (StringUtils.isNotBlank(name) && name.length() > 80) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "名称过长");
              }
              if (StringUtils.isNotBlank(description) && description.length() > 8192) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "描述过长");
              }
          }
      
          /**
           * 获取查询包装类
           *
           * @param interfaceInfoQueryRequest
           * @return
           */
          @Override
          public QueryWrapper<InterfaceInfo> getQueryWrapper(InterfaceInfoQueryRequest interfaceInfoQueryRequest) {
              QueryWrapper<InterfaceInfo> queryWrapper = new QueryWrapper<>();
              if (interfaceInfoQueryRequest == null) {
                  return queryWrapper;
              }
              Long id = interfaceInfoQueryRequest.getId();
              String searchText = interfaceInfoQueryRequest.getSearchText();
              String name = interfaceInfoQueryRequest.getName();
              String description = interfaceInfoQueryRequest.getDescription();
              String url = interfaceInfoQueryRequest.getUrl();
              String requestHeader = interfaceInfoQueryRequest.getRequestHeader();
              String responseHeader = interfaceInfoQueryRequest.getResponseHeader();
              Integer status = interfaceInfoQueryRequest.getStatus();
              String method = interfaceInfoQueryRequest.getMethod();
              Long userId = interfaceInfoQueryRequest.getUserId();
              String sortField = interfaceInfoQueryRequest.getSortField();
              String sortOrder = interfaceInfoQueryRequest.getSortOrder();
              // 拼接查询条件
              if (StringUtils.isNotBlank(searchText)) {
                  queryWrapper.like("title", searchText).or().like("content", searchText);
              }
              queryWrapper.like(StringUtils.isNotBlank(name), "name", name);
              queryWrapper.like(StringUtils.isNotBlank(description), "description", description);
              queryWrapper.like(StringUtils.isNotBlank(url), "url", url);
              queryWrapper.like(StringUtils.isNotBlank(requestHeader), "requestHeader", requestHeader);
              queryWrapper.like(StringUtils.isNotBlank(responseHeader), "responseHeader", responseHeader);
              queryWrapper.like(StringUtils.isNotBlank(method), "method", method);
              queryWrapper.eq(ObjectUtils.isNotEmpty(status), "status", status);
              queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
              queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
              queryWrapper.eq("isDelete", false);
              queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                      sortField);
              return queryWrapper;
          }
      
      //    @Override
      //    public Page<InterfaceInfo> searchFromEs(InterfaceInfoQueryRequest interfaceInfoQueryRequest) {
      //        Long id = interfaceInfoQueryRequest.getId();
      //        String name = interfaceInfoQueryRequest.getName();
      //        String searchText = interfaceInfoQueryRequest.getSearchText();
      //        String description = interfaceInfoQueryRequest.getDescription();
      //        String url = interfaceInfoQueryRequest.getUrl();
      //        String requestHeader = interfaceInfoQueryRequest.getRequestHeader();
      //        String responseHeader = interfaceInfoQueryRequest.getResponseHeader();
      //        Integer status = interfaceInfoQueryRequest.getStatus();
      //        String method = interfaceInfoQueryRequest.getMethod();
      //        Long userId = interfaceInfoQueryRequest.getUserId();
      //        // es 起始页为 0
      //        long current = interfaceInfoQueryRequest.getCurrent();
      //        long pageSize = interfaceInfoQueryRequest.getPageSize();
      //        String sortField = interfaceInfoQueryRequest.getSortField();
      //        String sortOrder = interfaceInfoQueryRequest.getSortOrder();
      //        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
      //        // 过滤
      //        boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
      //        if (id != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
      //        }
      //        if (notId != null) {
      //            boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
      //        }
      //        if (userId != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
      //        }
      //        // 必须包含所有标签
      //        if (CollectionUtils.isNotEmpty(tagList)) {
      //            for (String tag : tagList) {
      //                boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
      //            }
      //        }
      //        // 包含任何一个标签即可
      //        if (CollectionUtils.isNotEmpty(orTagList)) {
      //            BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
      //            for (String tag : orTagList) {
      //                orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
      //            }
      //            orTagBoolQueryBuilder.minimumShouldMatch(1);
      //            boolQueryBuilder.filter(orTagBoolQueryBuilder);
      //        }
      //        // 按关键词检索
      //        if (StringUtils.isNotBlank(searchText)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按标题检索
      //        if (StringUtils.isNotBlank(title)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按内容检索
      //        if (StringUtils.isNotBlank(content)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 排序
      //        SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
      //        if (StringUtils.isNotBlank(sortField)) {
      //            sortBuilder = SortBuilders.fieldSort(sortField);
      //            sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
      //        }
      //        // 分页
      //        PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
      //        // 构造查询
      //        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
      //                .withPageable(pageRequest).withSorts(sortBuilder).build();
      //        SearchHits<InterfaceInfoEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, InterfaceInfoEsDTO.class);
      //        Page<InterfaceInfo> page = new Page<>();
      //        page.setTotal(searchHits.getTotalHits());
      //        List<InterfaceInfo> resourceList = new ArrayList<>();
      //        // 查出结果后,从 db 获取最新动态数据(比如点赞数)
      //        if (searchHits.hasSearchHits()) {
      //            List<SearchHit<InterfaceInfoEsDTO>> searchHitList = searchHits.getSearchHits();
      //            List<Long> interfaceInfoIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
      //                    .collect(Collectors.toList());
      //            List<InterfaceInfo> interfaceInfoList = baseMapper.selectBatchIds(interfaceInfoIdList);
      //            if (interfaceInfoList != null) {
      //                Map<Long, List<InterfaceInfo>> idInterfaceInfoMap = interfaceInfoList.stream().collect(Collectors.groupingBy(InterfaceInfo::getId));
      //                interfaceInfoIdList.forEach(interfaceInfoId -> {
      //                    if (idInterfaceInfoMap.containsKey(interfaceInfoId)) {
      //                        resourceList.add(idInterfaceInfoMap.get(interfaceInfoId).get(0));
      //                    } else {
      //                        // 从 es 清空 db 已物理删除的数据
      //                        String delete = elasticsearchRestTemplate.delete(String.valueOf(interfaceInfoId), InterfaceInfoEsDTO.class);
      //                        log.info("delete interfaceInfo {}", delete);
      //                    }
      //                });
      //            }
      //        }
      //        page.setRecords(resourceList);
      //        return page;
      //    }
      
          @Override
          public InterfaceInfoVO getInterfaceInfoVO(InterfaceInfo interfaceInfo, HttpServletRequest request) {
              // 转为vo对象
              InterfaceInfoVO interfaceInfoVO = InterfaceInfoVO.objToVo(interfaceInfo);
              return interfaceInfoVO;
          }
      
          @Override
          public Page<InterfaceInfoVO> getInterfaceInfoVOPage(Page<InterfaceInfo> interfaceInfoPage, HttpServletRequest request) {
              List<InterfaceInfo> interfaceInfoList = interfaceInfoPage.getRecords();
              Page<InterfaceInfoVO> interfaceInfoVOPage = new Page<>(interfaceInfoPage.getCurrent(), interfaceInfoPage.getSize(), interfaceInfoPage.getTotal());
              if (CollectionUtils.isEmpty(interfaceInfoList)) {
                  return interfaceInfoVOPage;
              }
              List<InterfaceInfoVO> interfaceInfoVOList = interfaceInfoList.stream().map(interfaceInfo -> {
                  InterfaceInfoVO interfaceInfoVO = new InterfaceInfoVO();
                  BeanUtil.copyProperties(interfaceInfo,interfaceInfoVO);
                  return interfaceInfoVO;
              }).collect(Collectors.toList());
              // 2. 已登录,获取用户点赞、收藏状态
              // 填充信息
              interfaceInfoVOPage.setRecords(interfaceInfoVOList);
              return interfaceInfoVOPage;
          }
      }
      
      
      
      
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249

# 提供一个开放接口

# 创建一个模拟接口项目

  • 项目名称:gigotapi-interface

  • 我们现在有很多的接口信息,但全是假的,所以我们先来真实的发布一个给开发者使用的接口,创建一个模拟接口项目。

  • pom.xml

# 提供第一个测试接口查询名称接口

  • 提供三个不同的查询名称模拟接口:

    • GET接口
    • POST接口(param传参)
    • POST接口(Restful)
  • controller:查询名称api

    package top.panyuwen.gitgotapiinterface.controller;
    
    import org.springframework.web.bind.annotation.*;
    import top.panyuwen.gitgotapiinterface.model.User;
    
    /**
     * 查询名称 api
     * @author PYW
     */
    @RestController
    @RequestMapping("/name")
    public class NameController {
    
        @GetMapping()
        public String getNameByGet(String name){
            return "Get 你的名字是" + name;
        }
    
        @PostMapping()
        public String getNameByPost(@RequestParam String name){
            return "Post 你的名字是" + name;
        }
    
        @PostMapping("/user")
        public String getUsernameByPost(@RequestBody User user){
            return "Post 用户名字是" + user.getUsername();
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  • client:模拟调用接口

    package top.panyuwen.gitgotapiinterface.client;
    
    import cn.hutool.http.HttpRequest;
    import cn.hutool.http.HttpResponse;
    import cn.hutool.http.HttpUtil;
    import com.alibaba.fastjson.JSON;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestParam;
    import top.panyuwen.gitgotapiinterface.model.User;
    
    import java.util.HashMap;
    
    /**
     * 调用第三方客户端接口
     */
    public class APIClient {
    
        public String getNameByGet(String name){
            //可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
            HashMap<String, Object> paramMap = new HashMap<>();
            paramMap.put("name", name);
    
            String result= HttpUtil.get("http://localhost:8091/api/name", paramMap);
            System.out.println(result);
            return result;
    
        }
    
        public String getNameByPost(@RequestParam String name){
            //可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
            HashMap<String, Object> paramMap = new HashMap<>();
            paramMap.put("name", name);
            String result= HttpUtil.post("http://localhost:8091/api/name", paramMap);
            System.out.println(result);
            return result;
        }
    
        public String getUsernameByPost(@RequestBody User user){
            String json = JSON.toJSONString(user);
            System.out.println(json);
            HttpResponse response = HttpRequest.post("http://localhost:8091/api/name/user")
                    .body(json)
                    .execute();
            System.out.println(response.getStatus());
            String body = response.body();
            System.out.println(body);
            return body;
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
  • 测试类进行调用

    package top.panyuwen.gitgotapiinterface.client;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import top.panyuwen.gitgotapiinterface.model.User;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    @SpringBootTest
    class APIClientTest {
    
    
        @Test
        void APIClientTest(){
            APIClient apiClient = new APIClient();
            apiClient.getNameByGet("羊腿");
            apiClient.getNameByPost("羊腿");
            User user = new User();
            user.setUsername("羊腿");
            apiClient.getUsernameByPost(user);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    image-20231220143742484

# API签名认证

  • api签名认证流程:(其实和腾讯云阿里云的accessKey secretKey一样)

    1. 签发签名

    2. 使用签名(校验签名)

  • 应用场景

    1. 保证安全,不能随便一个人调用
    2. 没有用户登录
    3. 无状态登录

注:千万不要直接在服务器之间传递accessKey secretKey,有可能会被拦截

  • 因为不能直接传输,因此需要签名算法得到一个不可解密的值

    • 用户参数+秘钥 => 签名生成算法 => 不可解密的值
  • 解决重放问题

    1. 加一个随机数,后端用过一次就不会再用这个随机数了
      • 弊端:要保存没用过的随机数
    2. 加一个timesamp时间戳,校验时间戳是否过期。
    3. 腾讯云解决策略,只在创建API秘钥时提供用户,之后不可进行查询
  • 最终请求头参数,注:secretKey一定不能传输到后端去!

        /**
         * 通过POST请求获取用户名
         *
         * @param user 用户信息
         * @return 用户名
         */
        public String getUsernameByPost(@RequestBody User user) {
            String json = JSON.toJSONString(user);
            System.out.println(json);
            HttpResponse response = HttpRequest.post("http://localhost:8091/api/name/user")
                    .addHeaders(getHeaderMap(json))
                    .header("Content-Type", "application/json; charset=UTF-8")
                    .body(json)
                    .execute();
            System.out.println(response.getStatus());
            String body = response.body();
            System.out.println(body);
            return body;
        }
    
        /**
         * 获取请求头Map
         * @param body 请求体
         * @return 请求头Map
         */
        private Map<String, String> getHeaderMap(String body) {
            Map<String, String> headerMap = new HashMap<>();
            headerMap.put("X-SecretId", secretId);
            // 一定不能发送给后端, 后端会自动计算签名
    //        headerMap.put("X-SecretKey", secretKey);
            headerMap.put("X-Nonce", RandomUtil.randomNumbers(4));
            headerMap.put("X-Body", body);
            headerMap.put("X-Timestamp", System.currentTimeMillis() + "");
            headerMap.put("X-Sign", SignUtils.getSign(body, secretKey));
            return headerMap;
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
  • 改造服务端controller解析API密钥

    糊涂包只能传英文,估计是请求头带utf-8设置错误了,这儿不纠结了后续解决

        /**
         * 通过POST请求获取用户名
         * @param user 用户信息
         * @param request HTTP请求对象
         * @return 用户名
         */
        @PostMapping("/user")
        public String getUsernameByPost(@RequestBody User user, HttpServletRequest request){
            String secretId = request.getHeader("X-SecretId");
            String nonce = request.getHeader("X-Nonce");
            String body = request.getHeader("X-Body");
            String timestamp = request.getHeader("X-Timestamp");
            String sign = request.getHeader("X-Sign");
            
            // 是否分配给用户校验 后续补齐 这儿为了演示直接给值了
            if(!"123456".equals(secretId)){
                throw new RuntimeException("API密钥不正确");
            }
            
            // 随机数不得大于一万
            if(Integer.parseInt(nonce) > 10000){
                throw new RuntimeException("随机数不得大于一万");
            }
            
            // 时间和当前时间不得超过五分钟
            if(System.currentTimeMillis() - Long.parseLong(timestamp) > 300000){
                throw new RuntimeException("时间和当前时间不得超过五分钟");
            }
            
            // 解析sign,查询secretKey是否和header中的一致 这儿为了演示secretKey直接给值
            String signInServer = SignUtils.getSign(body, "654321");
            if(!sign.equals(signInServer)){
                throw new RuntimeException("签名不正确");
            }
            
            return "Post 用户名字是" + user.getUsername();
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

# 开发一个SDK

**API签名认证开发遗留问题:**现在开发者调用我们的接口每次都需要加一大堆请求头参数(随机数,时间戳.....),因此为了简化开发我们需要提供SDK

开发starter的好处(例如redis-starter,spring-boot-starter):开发者引入后,可以直接在application.yml中写配置,自动创建客户端

SDK开发流程

  • 创建一个新的空项目gigotapi-client-sdk

  • pom文件修改

    1. 既然都写sdk了,那么一定要规范版本号

      image-20231221172926449

    2. 删除通过maven构建项目代码,一定删除不然会报错,我们是构建依赖包而不是直接运行的jar项目所以需要删掉

      image.png

    3. spring-boot-configuration-processor:支持使用@ConfigurationProperties 注解的类,并自动生成配置类和代码提示。示例:第三方调用sdk时可以直接通过yml配置

      image-20231221171440816

    4. 其他项目所需要的依赖

    5. 完整依赖如下:pom.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <groupId>top.panyuwen</groupId>
          <artifactId>gigotapi-client-sdk</artifactId>
          <version>0.0.1</version>
          <name>gigotapi-client-sdk</name>
          <description>gigotapi-client-sdk</description>
          <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>2.7.2</version>
              <relativePath/> <!-- lookup parent from repository -->
          </parent>
          <properties>
              <java.version>1.8</java.version>
          </properties>
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter</artifactId>
              </dependency>
      
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-configuration-processor</artifactId>
                  <optional>true</optional>
              </dependency>
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
                  <optional>true</optional>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
              <dependency>
                  <groupId>cn.hutool</groupId>
                  <artifactId>hutool-all</artifactId>
                  <version>5.8.8</version>
              </dependency>
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>fastjson</artifactId>
                  <version>1.2.76</version>
              </dependency>
          </dependencies>
      </project>
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
  • 删除Springboot的主类,我们这儿不是运行web项目,而是提供一个依赖包给客户使用

  • 创建配置类:我们希望用户能够通过引入 starter 的方式直接使用客户端,而不需要手动创建,所以我们需要编写一个配置类。

    1. 创建配置类GigotAPIClientConfig

      **@ConfigurationProperties("gigot-api.client"):**能够读取application.yml的配置,读取到配置之后,把这个读到的配置设置到我们这里的属性中,

      package top.panyuwen.gigotapiclientsdk;
      
      import lombok.Data;
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;
      import top.panyuwen.gigotapiclientsdk.client.GigotApiClient;
      
      /**
       * 配置类,用于配置Gigot API客户端的相关参数
       * @author PYW
       */
      @Configuration
      @ConfigurationProperties("gigot-api.client")
      @Data
      @ComponentScan
      public class GigotApiClientConfig {
      
          /**
           * secretId
           */
          private String secretId;
      
          /**
           * secretId
           */
          private String secretKey;
      
          /**
           * 返回GigotApiClient的Bean
           *
           * @return GigotApiClient对象
           */
          @Bean
          public GigotApiClient gigotApiClient(){
              return new GigotApiClient(secretId, secretKey);
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
  • SDK核心业务代码编写

    1. 我们这个项目主要解决的是客户调用时不用复杂的配置请求,因此直接把gigotapi-interface中的业务代码拿来用就好了,复制client,model,utils到新项目中
  • 创建spring.factories

    1. 地址:src/main/resources/META-INF/spring.factories

    2. spring.factories文件:定义了 Spring Boot 的自动配置。它是一个标准的 Java Properties 文件,用于指定要自动配置的类。这个文件中的每一行都是一个配置项,包含两个部分:配置项的全限定类名和对应的自动配置类。它们之间使用等号(=)进行分隔。在 Spring Boot 应用启动时,它会加载这个 spring.factories 文件,并根据其中的配置项自动进行相应的配置。

    3. 绑定配置类

      spring.factories

      # spring boot starter
      org.springframework.boot.autoconfigure.EnableAutoConfiguration=top.panyuwen.gigotapiclientsdk.GigotApiClientConfig
      
      1
      2
  • 删除或者屏蔽测试类:因为主类都被咱们删了测试类当然不行了

  • 执行maven的install,打包在本地,这个时候本地的项目就可以直接引入了,如果要团队协作可以传到云maven中,或者在maven官网注册账号传上去

  • 调用sdk

    1. 引入依赖

              <dependency>
                  <groupId>top.panyuwen</groupId>
                  <artifactId>gigotapi-client-sdk</artifactId>
                  <version>0.0.1</version>
              </dependency>
      
      1
      2
      3
      4
      5
    2. yml配置,这下还可以看到yml的提示了

      image-20231221174952824

    3. 调用sdk中的apiClient

      package top.panyuwen.gitgotapiinterface.client;
      
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import top.panyuwen.gigotapiclientsdk.client.GigotApiClient;
      import top.panyuwen.gigotapiclientsdk.model.User;
      
      import static org.junit.jupiter.api.Assertions.*;
      
      @SpringBootTest
      class APIClientTest {
      
          @Autowired
          private GigotApiClient gigotApiClient;
      
          @Test
          void APIClientTest(){
              User user = new User();
              user.setUsername("gigot");
              gigotApiClient.getUsernameByPost(user);
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23

      image-20231221175201698

# 用户管理

其实这个功能已经做过很多次了,因此直接上源码

  • 增删改查

    • UserContorller

      package top.panyuwen.gigotapi.controller;
      
      import cn.hutool.core.collection.CollUtil;
      import cn.hutool.core.util.ObjUtil;
      import cn.hutool.core.util.StrUtil;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Value;
      import top.panyuwen.gigotapi.annotation.AuthCheck;
      import top.panyuwen.gigotapi.common.BaseResponse;
      import top.panyuwen.gigotapi.common.request.DeleteListRequest;
      import top.panyuwen.gigotapi.common.request.DeleteRequest;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.common.ResultUtils;
      import top.panyuwen.gigotapi.config.WxOpenConfig;
      import top.panyuwen.gigotapi.constant.UserConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.model.dto.user.UserAddRequest;
      import top.panyuwen.gigotapi.model.dto.user.UserLoginRequest;
      import top.panyuwen.gigotapi.model.dto.user.UserQueryRequest;
      import top.panyuwen.gigotapi.model.dto.user.UserRegisterRequest;
      import top.panyuwen.gigotapi.model.dto.user.UserUpdateMyRequest;
      import top.panyuwen.gigotapi.model.dto.user.UserUpdateRequest;
      import top.panyuwen.gigotapi.model.entity.InterfaceInfo;
      import top.panyuwen.gigotapi.model.entity.User;
      import top.panyuwen.gigotapi.model.vo.LoginUserVO;
      import top.panyuwen.gigotapi.model.vo.UserVO;
      import top.panyuwen.gigotapi.service.FileService;
      import top.panyuwen.gigotapi.service.UserService;
      
      import java.util.List;
      import java.util.stream.Collectors;
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      
      import lombok.extern.slf4j.Slf4j;
      import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
      import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
      import me.chanjar.weixin.mp.api.WxMpService;
      import org.apache.commons.lang3.StringUtils;
      import org.springframework.beans.BeanUtils;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.PostMapping;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestParam;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * 用户接口
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @RestController
      @RequestMapping("/user")
      @Slf4j
      public class UserController {
      
          @Value("${gigot-api.upload.apiUrl}")
          private String apiUrl;
      
          @Resource
          private UserService userService;
      
          @Resource
          private WxOpenConfig wxOpenConfig;
      
          @Autowired
          private FileService fileService;
      
          // region 登录相关
      
          /**
           * 用户注册
           *
           * @param userRegisterRequest
           * @return
           */
          @PostMapping("/register")
          public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
              if (userRegisterRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              String userAccount = userRegisterRequest.getUserAccount();
              String userPassword = userRegisterRequest.getUserPassword();
              String checkPassword = userRegisterRequest.getCheckPassword();
              if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
                  return null;
              }
              long result = userService.userRegister(userAccount, userPassword, checkPassword);
              return ResultUtils.success(result);
          }
      
          /**
           * 用户登录
           *
           * @param userLoginRequest
           * @param request
           * @return
           */
          @PostMapping("/login")
          public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
              if (userLoginRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              String userAccount = userLoginRequest.getUserAccount();
              String userPassword = userLoginRequest.getUserPassword();
              if (StringUtils.isAnyBlank(userAccount, userPassword)) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);
              return ResultUtils.success(loginUserVO);
          }
      
          /**
           * 用户登录(微信开放平台)
           */
          @GetMapping("/login/wx_open")
          public BaseResponse<LoginUserVO> userLoginByWxOpen(HttpServletRequest request, HttpServletResponse response,
                                                             @RequestParam("code") String code) {
              WxOAuth2AccessToken accessToken;
              try {
                  WxMpService wxService = wxOpenConfig.getWxMpService();
                  accessToken = wxService.getOAuth2Service().getAccessToken(code);
                  WxOAuth2UserInfo userInfo = wxService.getOAuth2Service().getUserInfo(accessToken, code);
                  String unionId = userInfo.getUnionId();
                  String mpOpenId = userInfo.getOpenid();
                  if (StringUtils.isAnyBlank(unionId, mpOpenId)) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
                  }
                  return ResultUtils.success(userService.userLoginByMpOpen(userInfo, request));
              } catch (Exception e) {
                  log.error("userLoginByWxOpen error", e);
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
              }
          }
      
          /**
           * 用户注销
           *
           * @param request
           * @return
           */
          @PostMapping("/logout")
          public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
              if (request == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              boolean result = userService.userLogout(request);
              return ResultUtils.success(result);
          }
      
          /**
           * 获取当前登录用户
           *
           * @param request
           * @return
           */
          @GetMapping("/get/login")
          public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {
              User user = userService.getLoginUser(request);
              return ResultUtils.success(userService.getLoginUserVO(user));
          }
      
          // endregion
      
          // region 增删改查
      
          /**
           * 创建用户
           *
           * @param userAddRequest
           * @param request
           * @return
           */
          @PostMapping("/add")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) {
              if (userAddRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = new User();
              BeanUtils.copyProperties(userAddRequest, user);
              boolean result = userService.save(user);
              ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
              return ResultUtils.success(user.getId());
          }
      
          /**
           * 删除用户
           *
           * @param deleteRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteById")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> deleteUserById(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
              if (deleteRequest == null || deleteRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              boolean b = userService.removeById(deleteRequest.getId());
              return ResultUtils.success(b);
          }
      
          /**
           * 删除用户(多个id)
           *
           * @param deleteListRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteByIds")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> deleteUserByIds(@RequestBody DeleteListRequest deleteListRequest, HttpServletRequest request) {
              if (deleteListRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              List<Long> ids = deleteListRequest.getIds();
              // 判断是否存在
              List<User> users = userService.listByIds(ids);
              if (users.size() != ids.size()) {
                  List<Long> idsInDB = users.stream().map(User::getId).collect(Collectors.toList());
                  List<Long> differentIds = CollUtil.disjunction(ids, idsInDB).stream().collect(Collectors.toList());
                  log.error("differentIds:{}", differentIds);
                  ThrowUtils.throwIf(differentIds.size() > 0, ErrorCode.NOT_FOUND_ERROR, "未找到删除数据:differentIds:" + differentIds);
              }
              // 校验是否为管理员
              if (!userService.isAdmin(request)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "只允许管理员删除");
              }
              boolean removeByIdsFlag = userService.removeByIds(ids);
              if (!removeByIdsFlag) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据库删除异常");
              }
              return ResultUtils.success(removeByIdsFlag);
          }
      
          /**
           * 更新用户
           *
           * @param userUpdateRequest
           * @param request
           * @return
           */
          @PostMapping("/update")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest,
                                                  HttpServletRequest request) {
              if (userUpdateRequest == null || userUpdateRequest.getId() == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              // 查询用户信息
              User userInDB = userService.getById(userUpdateRequest.getId());
              if (ObjUtil.isEmpty(userInDB)) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未查询到修改信息");
              }
              User user = new User();
              BeanUtils.copyProperties(userUpdateRequest, user);
              boolean result = userService.updateById(user);
              ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
              // 删除用户头像
              String defaultAvatarUrl = apiUrl + "/api/file/downloadAvatar?name=defalut-avatar.jpg";
              // 用户头像删除校验条件
              String avatarUrlInDB = userInDB.getUserAvatar();
              boolean deleteAvatarFlag =
                      StrUtil.isNotBlank(avatarUrlInDB) &&
                              ! avatarUrlInDB.equals(user.getUserAvatar()) &&
                              ! defaultAvatarUrl.equals(user.getUserAvatar());
              log.info("准备删除的头像文件名:{} , 判断条件deleteAvatarFlag:{}",avatarUrlInDB,deleteAvatarFlag);
              if (deleteAvatarFlag) {
                  //删除
                  log.debug("准备删除的头像文件名:{}",avatarUrlInDB);
                  boolean isDelete = fileService.deleteFile(avatarUrlInDB);
                  if(!isDelete){
                      throw new BusinessException(ErrorCode.PARAMS_ERROR,"更新失败,用户头像删除失败");
                  }
              }
              return ResultUtils.success(true);
          }
      
          /**
           * 根据 id 获取用户(仅管理员)
           *
           * @param id
           * @param request
           * @return
           */
          @GetMapping("/get")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<User> getUserById(long id, HttpServletRequest request) {
              if (id <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getById(id);
              ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);
              return ResultUtils.success(user);
          }
      
          /**
           * 根据 id 获取包装类
           *
           * @param id
           * @param request
           * @return
           */
          @GetMapping("/get/vo")
          public BaseResponse<UserVO> getUserVOById(long id, HttpServletRequest request) {
              BaseResponse<User> response = getUserById(id, request);
              User user = response.getData();
              return ResultUtils.success(userService.getUserVO(user));
          }
      
          /**
           * 分页获取用户列表(仅管理员)
           *
           * @param userQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/list/page")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Page<User>> listUserByPage(@RequestBody UserQueryRequest userQueryRequest,
                                                         HttpServletRequest request) {
              long current = userQueryRequest.getCurrent();
              long size = userQueryRequest.getPageSize();
              Page<User> userPage = userService.page(new Page<>(current, size),
                      userService.getQueryWrapper(userQueryRequest));
              return ResultUtils.success(userPage);
          }
      
          /**
           * 分页获取用户封装列表
           *
           * @param userQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/list/page/vo")
          public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest,
                                                             HttpServletRequest request) {
              if (userQueryRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              long current = userQueryRequest.getCurrent();
              long size = userQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              Page<User> userPage = userService.page(new Page<>(current, size),
                      userService.getQueryWrapper(userQueryRequest));
              Page<UserVO> userVOPage = new Page<>(current, size, userPage.getTotal());
              List<UserVO> userVO = userService.getUserVO(userPage.getRecords());
              userVOPage.setRecords(userVO);
              return ResultUtils.success(userVOPage);
          }
      
          // endregion
      
          /**
           * 更新个人信息
           *
           * @param userUpdateMyRequest
           * @param request
           * @return
           */
          @PostMapping("/update/my")
          public BaseResponse<Boolean> updateMyUser(@RequestBody UserUpdateMyRequest userUpdateMyRequest,
                                                    HttpServletRequest request) {
              if (userUpdateMyRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User loginUser = userService.getLoginUser(request);
              User user = new User();
              BeanUtils.copyProperties(userUpdateMyRequest, user);
              user.setId(loginUser.getId());
              boolean result = userService.updateById(user);
              ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
              return ResultUtils.success(true);
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
    • UserServiceImpl

      package top.panyuwen.gigotapi.service.impl;
      
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
      import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
      import org.springframework.beans.factory.annotation.Value;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.constant.CommonConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.mapper.UserMapper;
      import top.panyuwen.gigotapi.model.dto.user.UserQueryRequest;
      import top.panyuwen.gigotapi.model.entity.User;
      import top.panyuwen.gigotapi.model.enums.UserRoleEnum;
      import top.panyuwen.gigotapi.model.vo.LoginUserVO;
      import top.panyuwen.gigotapi.model.vo.UserVO;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.SqlUtils;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.stream.Collectors;
      import javax.servlet.http.HttpServletRequest;
      import lombok.extern.slf4j.Slf4j;
      import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
      import org.apache.commons.lang3.StringUtils;
      import org.springframework.beans.BeanUtils;
      import org.springframework.stereotype.Service;
      import org.springframework.util.DigestUtils;
      import top.panyuwen.gigotapi.constant.UserConstant;
      
      /**
       * 用户服务实现
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @Service
      @Slf4j
      public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
      
          /**
           * 盐值,混淆密码
           */
          private static final String SALT = "gigot_";
      
          @Value("${gigot-api.upload.apiUrl}")
          private String apiUrl;
      
          @Override
          public long userRegister(String userAccount, String userPassword, String checkPassword) {
              // 1. 校验
              if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
              }
              if (userAccount.length() < 4) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
              }
              if (userPassword.length() < 6 || checkPassword.length() < 6) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
              }
              // 密码和校验密码相同
              if (!userPassword.equals(checkPassword)) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
              }
              synchronized (userAccount.intern()) {
                  // 账户不能重复
                  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                  queryWrapper.eq("userAccount", userAccount);
                  long count = this.baseMapper.selectCount(queryWrapper);
                  if (count > 0) {
                      throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
                  }
                  // 2. 加密
                  String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
                  log.info("注册加密:{}",encryptPassword);
                  // 3. 插入数据
                  User user = new User();
                  user.setUserAccount(userAccount);
                  user.setUserPassword(encryptPassword);
                  user.setUserAvatar(apiUrl + "/api/common/downloadAvatar?name=defalut-avatar.jpg");
                  user.setUserName("gigotApi_"+userAccount);
                  user.setUserProfile("这个人很神秘,什么都没有写");
                  boolean saveResult = this.save(user);
                  if (!saveResult) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
                  }
                  return user.getId();
              }
          }
      
          @Override
          public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {
              // 1. 校验
              if (StringUtils.isAnyBlank(userAccount, userPassword)) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
              }
              if (userAccount.length() < 4) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");
              }
              if (userPassword.length() < 6) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码不小于6位");
              }
              // 2. 加密
              String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
              log.info("登录解密:{}",encryptPassword);
              // 查询用户是否存在
              QueryWrapper<User> queryWrapper = new QueryWrapper<>();
              queryWrapper.eq("userAccount", userAccount);
              queryWrapper.eq("userPassword", encryptPassword);
              User user = this.baseMapper.selectOne(queryWrapper);
              // 用户不存在
              if (user == null) {
                  log.info("user login failed, userAccount cannot match userPassword");
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
              }
              // 3. 记录用户的登录态
              request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
              return this.getLoginUserVO(user);
          }
      
          @Override
          public LoginUserVO userLoginByMpOpen(WxOAuth2UserInfo wxOAuth2UserInfo, HttpServletRequest request) {
              String unionId = wxOAuth2UserInfo.getUnionId();
              String mpOpenId = wxOAuth2UserInfo.getOpenid();
              // 单机锁
              synchronized (unionId.intern()) {
                  // 查询用户是否已存在
                  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                  queryWrapper.eq("unionId", unionId);
                  User user = this.getOne(queryWrapper);
                  // 被封号,禁止登录
                  if (user != null && UserRoleEnum.BAN.getValue().equals(user.getUserRole())) {
                      throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "该用户已被封,禁止登录");
                  }
                  // 用户不存在则创建
                  if (user == null) {
                      user = new User();
                      user.setUnionId(unionId);
                      user.setMpOpenId(mpOpenId);
                      user.setUserAvatar(wxOAuth2UserInfo.getHeadImgUrl());
                      user.setUserName(wxOAuth2UserInfo.getNickname());
                      boolean result = this.save(user);
                      if (!result) {
                          throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败");
                      }
                  }
                  // 记录用户的登录态
                  request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
                  return getLoginUserVO(user);
              }
          }
      
          /**
           * 获取当前登录用户
           *
           * @param request
           * @return
           */
          @Override
          public User getLoginUser(HttpServletRequest request) {
              // 先判断是否已登录
              Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
              User currentUser = (User) userObj;
              if (currentUser == null || currentUser.getId() == null) {
                  throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
              }
              // 从数据库查询(追求性能的话可以注释,直接走缓存)
              long userId = currentUser.getId();
              currentUser = this.getById(userId);
              if (currentUser == null) {
                  throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
              }
              return currentUser;
          }
      
          /**
           * 获取当前登录用户(允许未登录)
           *
           * @param request
           * @return
           */
          @Override
          public User getLoginUserPermitNull(HttpServletRequest request) {
              // 先判断是否已登录
              Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
              User currentUser = (User) userObj;
              if (currentUser == null || currentUser.getId() == null) {
                  return null;
              }
              // 从数据库查询(追求性能的话可以注释,直接走缓存)
              long userId = currentUser.getId();
              return this.getById(userId);
          }
      
          /**
           * 是否为管理员
           *
           * @param request
           * @return
           */
          @Override
          public boolean isAdmin(HttpServletRequest request) {
              // 仅管理员可查询
              Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
              User user = (User) userObj;
              return isAdmin(user);
          }
      
          @Override
          public boolean isAdmin(User user) {
              return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
          }
      
          /**
           * 用户注销
           *
           * @param request
           */
          @Override
          public boolean userLogout(HttpServletRequest request) {
              if (request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE) == null) {
                  throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
              }
              // 移除登录态
              request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE);
              return true;
          }
      
          @Override
          public LoginUserVO getLoginUserVO(User user) {
              if (user == null) {
                  return null;
              }
              LoginUserVO loginUserVO = new LoginUserVO();
              BeanUtils.copyProperties(user, loginUserVO);
              return loginUserVO;
          }
      
          @Override
          public UserVO getUserVO(User user) {
              if (user == null) {
                  return null;
              }
              UserVO userVO = new UserVO();
              BeanUtils.copyProperties(user, userVO);
              return userVO;
          }
      
          @Override
          public List<UserVO> getUserVO(List<User> userList) {
              if (CollectionUtils.isEmpty(userList)) {
                  return new ArrayList<>();
              }
              return userList.stream().map(this::getUserVO).collect(Collectors.toList());
          }
      
          @Override
          public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {
              if (userQueryRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
              }
              Long id = userQueryRequest.getId();
              String unionId = userQueryRequest.getUnionId();
              String mpOpenId = userQueryRequest.getMpOpenId();
              String userName = userQueryRequest.getUserName();
              String userProfile = userQueryRequest.getUserProfile();
              String userRole = userQueryRequest.getUserRole();
              String sortField = userQueryRequest.getSortField();
              String sortOrder = userQueryRequest.getSortOrder();
              QueryWrapper<User> queryWrapper = new QueryWrapper<>();
              queryWrapper.eq(id != null, "id", id);
              queryWrapper.eq(StringUtils.isNotBlank(unionId), "unionId", unionId);
              queryWrapper.eq(StringUtils.isNotBlank(mpOpenId), "mpOpenId", mpOpenId);
              queryWrapper.eq(StringUtils.isNotBlank(userRole), "userRole", userRole);
              queryWrapper.like(StringUtils.isNotBlank(userProfile), "userProfile", userProfile);
              queryWrapper.like(StringUtils.isNotBlank(userName), "userName", userName);
              queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                      sortField);
              return queryWrapper;
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
  • 头像上传下载删除

    • FileController

      package top.panyuwen.gigotapi.controller;
      
      import cn.hutool.core.io.FileUtil;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.web.bind.annotation.*;
      import top.panyuwen.gigotapi.common.BaseResponse;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.common.ResultUtils;
      import top.panyuwen.gigotapi.constant.FileConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.manager.CosManager;
      import top.panyuwen.gigotapi.model.dto.file.UploadFileRequest;
      import top.panyuwen.gigotapi.model.entity.User;
      import top.panyuwen.gigotapi.model.enums.FileUploadBizEnum;
      import top.panyuwen.gigotapi.service.FileService;
      import top.panyuwen.gigotapi.service.UserService;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.util.Arrays;
      import javax.annotation.Resource;
      import javax.servlet.ServletOutputStream;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      
      import lombok.extern.slf4j.Slf4j;
      import org.apache.commons.lang3.RandomStringUtils;
      import org.springframework.web.multipart.MultipartFile;
      import top.panyuwen.gigotapi.utils.GetAbsolutePathUtils;
      
      /**
       * 文件接口
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @RestController
      @RequestMapping("/file")
      @Slf4j
      public class FileController {
      
          @Value("${gigot-api.upload.avatarUrlFilePath}")
          private String filePath;
      
          @Resource
          private UserService userService;
      
          @Resource
          private CosManager cosManager;
      
          @Autowired
          private FileService fileService;
      
          /**
           * 文件上传
           *
           * @param file
           * @return
           */
          //形参名称必须与请求体里的名称保持一
          @PostMapping("/uploadAvatar")
          public BaseResponse<String> uploadAvatar(MultipartFile file) throws IOException {
              //file是一个临时文件,否则本次请求完成后临时文件会被删除
              log.info("上传文件,接收到的前端数据为file:{}", file);
              if (file == null) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "传输的file为空");
              }
              String fileName = fileService.uploadAvatar(file);
              return ResultUtils.success(fileName);
          }
      
          /**
           * 文件下载
           *
           * @param name
           */
          @GetMapping("/downloadAvatar")
          public void downloadAvatar(@RequestParam String name, HttpServletResponse resp) throws IOException {
              log.info("下载文件,接收到的前端数据为{}", name);
              String absolutePath = GetAbsolutePathUtils.getAbsolutePathUtil(filePath);
              //输入流,通过输入流读取文件内容
              FileInputStream fileInputStream = new FileInputStream(new File(absolutePath + File.separator + name));
      
              //输出流,输出文件返回给前端
              ServletOutputStream outputStream = resp.getOutputStream();
      
              //设置响应时一个image文件
              resp.setContentType("image/jpeg");
      
              // 读取本地文件
              int len = 0;
              byte[] bytes = new byte[1024];
              while ((len = fileInputStream.read(bytes)) != -1) {
                  outputStream.write(bytes, 0, len);
                  outputStream.flush();
              }
      
              //关闭资源
              fileInputStream.close();
              outputStream.close();
      
          }
      
          /**
           * 文件上传(腾讯云 COS)
           *
           * @param multipartFile
           * @param uploadFileRequest
           * @param request
           * @return
           */
          @PostMapping("/uploadForTencentCos")
          public BaseResponse<String> uploadFileForTencentCos(@RequestPart("file") MultipartFile multipartFile,
                  UploadFileRequest uploadFileRequest, HttpServletRequest request) {
              String biz = uploadFileRequest.getBiz();
              FileUploadBizEnum fileUploadBizEnum = FileUploadBizEnum.getEnumByValue(biz);
              if (fileUploadBizEnum == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              validFile(multipartFile, fileUploadBizEnum);
              User loginUser = userService.getLoginUser(request);
              // 文件目录:根据业务、用户来划分
              String uuid = RandomStringUtils.randomAlphanumeric(8);
              String filename = uuid + "-" + multipartFile.getOriginalFilename();
              String filepath = String.format("/%s/%s/%s", fileUploadBizEnum.getValue(), loginUser.getId(), filename);
              File file = null;
              try {
                  // 上传文件
                  file = File.createTempFile(filepath, null);
                  multipartFile.transferTo(file);
                  cosManager.putObject(filepath, file);
                  // 返回可访问地址
                  return ResultUtils.success(FileConstant.COS_HOST + filepath);
              } catch (Exception e) {
                  log.error("file upload error, filepath = " + filepath, e);
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
              } finally {
                  if (file != null) {
                      // 删除临时文件
                      boolean delete = file.delete();
                      if (!delete) {
                          log.error("file delete error, filepath = {}", filepath);
                      }
                  }
              }
          }
      
          /**
           * 校验文件
           *
           * @param multipartFile
           * @param fileUploadBizEnum 业务类型
           */
          private void validFile(MultipartFile multipartFile, FileUploadBizEnum fileUploadBizEnum) {
              // 文件大小
              long fileSize = multipartFile.getSize();
              // 文件后缀
              String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());
              final long ONE_M = 1024 * 1024L;
              if (FileUploadBizEnum.USER_AVATAR.equals(fileUploadBizEnum)) {
                  if (fileSize > ONE_M) {
                      throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小不能超过 1M");
                  }
                  if (!Arrays.asList("jpeg", "jpg", "svg", "png", "webp").contains(fileSuffix)) {
                      throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件类型错误");
                  }
              }
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
    • FileServiceImpl

      package top.panyuwen.gigotapi.service.impl;
      
      import cn.hutool.core.io.FileUtil;
      import cn.hutool.core.util.StrUtil;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.stereotype.Service;
      import org.springframework.web.multipart.MultipartFile;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.service.FileService;
      import top.panyuwen.gigotapi.utils.GetAbsolutePathUtils;
      
      import java.io.File;
      import java.io.IOException;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.util.UUID;
      import java.util.regex.Matcher;
      import java.util.regex.Pattern;
      
      /**
       *  文件上传服务器
       * @author PYW
       * @description: 文件上传服务实现类
       */
      @Slf4j
      @Service
      public class FileServiceImpl implements FileService {
      
      
          @Value("${gigot-api.upload.avatarUrlFilePath}")
          private String avatarFilePath;
      
      
      
          @Override
          public String uploadAvatar(MultipartFile file) throws IOException {
              //获取原始文件名
              String originalFileName = file.getOriginalFilename();
              //获取文件后缀名
              String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
              System.out.println(suffix);
      
              if(! StrUtil.endWithAny(suffix,".jpg", ".png",".jpeg")){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR,"请上传图片,仅支持以下图片格式:gif|png|jpg|jpeg|webp|svg|psd|bmp|tif");
              }
      
      
      
              //使用uuid重新生成文件名,防止文件名称重复造成文件覆盖
              String fileName = UUID.randomUUID().toString() + suffix;
              log.info("上传文件的原始图片文件名为:{}  根据UUid生成的文件名为:{}",originalFileName,fileName);
      
              //获取绝对路径
              String absolutePath = GetAbsolutePathUtils.getAbsolutePathUtil(avatarFilePath);
              //存储文件到服务器
              log.info("上传文件存储完整路径为:{}",absolutePath.toString()+ File.separator+fileName);
              file.transferTo(new File(absolutePath.toString()+File.separator+fileName));
              return fileName;
          }
      
          @Override
          public boolean deleteFile(String url) {
              // 判断地址文件夹
              String filePath = urlLocationHandler(url);
              return FileUtil.del(filePath);
          }
      
          /**
           * 解析地址在哪个文件夹下
           * @param urlForCustorm
           */
          public String urlLocationHandler(String urlForCustorm) {
              String result = null;
              //非空判断
              if(StrUtil.isBlank(urlForCustorm)){
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"传输的地址为空");
              }
      
              // 地址转为Url对象
              URL url = null;
              try {
                  url = new URL(urlForCustorm);
              } catch (MalformedURLException e) {
                  e.printStackTrace();
              }
              String path = url.getPath();
              String query = url.getQuery();
      
      
              // 根据请求路径判断删除的是哪个文件下的数据
              // 头像
              if(path.equals("/api/file/downloadAvatar")){
                  result = avatarFilePath + "/";
              }
      
              // 路径校验完成后判断result是否为空
              if(StrUtil.isBlank(result)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR,"路径错误,无法找到文件位置");
              }
      
      
              // 获取文件名称
              // 使用正则表达式提取文件名
              Pattern pattern = Pattern.compile("name=([^&]+)");
              Matcher matcher = pattern.matcher(query);
              if (!matcher.find()) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR,"文件名称参数错误,无法找到文件位置");
              }
              String fileName = matcher.group(1);
              result = result + fileName;
              if(StrUtil.isBlank(result)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR,"路径错误,无法找到文件位置");
              }
      
              // 获取文件绝对路径
              result = GetAbsolutePathUtils.getAbsolutePathUtil(result);
              log.debug("解析地址成功!文件绝对路径 result:{}",result);
              return result;
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122

# 用户接口对应关系

  • 因为我们要统计用户每个接口的调用次数等信息,后续还有统计功能因此我们需要添加一个用户接口对应关系表

  • 用户和接口是多对多的关系,所以需要添加一张关系表

    /*
     Navicat Premium Data Transfer
    
     Source Server         : localhost
     Source Server Type    : MySQL
     Source Server Version : 80033 (8.0.33)
     Source Host           : localhost:3306
     Source Schema         : gigot_api
    
     Target Server Type    : MySQL
     Target Server Version : 80033 (8.0.33)
     File Encoding         : 65001
    
     Date: 03/01/2024 12:36:14
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for user_interfaceinfo
    -- ----------------------------
    DROP TABLE IF EXISTS `user_interfaceinfo`;
    CREATE TABLE `user_interfaceinfo`  (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
      `userId` bigint NOT NULL COMMENT '调用人id',
      `interfaceId` bigint NOT NULL COMMENT '接口id',
      `totalInvokes` bigint NOT NULL DEFAULT 0 COMMENT '总调用次数',
      `status` tinyint NOT NULL DEFAULT 0 COMMENT '调用状态(0- 正常 1- 禁用·)',
      `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `isDelete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户接口调用表' ROW_FORMAT = DYNAMIC;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
  • 然后就是无聊的增删改查开发,实体类和vodto等就不放上来了,可以直接去看我的git

    • Contorller

      package top.panyuwen.gigotapi.controller;
      
      import cn.hutool.core.collection.CollUtil;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.BeanUtils;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.*;
      import top.panyuwen.gigotapi.annotation.AuthCheck;
      import top.panyuwen.gigotapi.common.BaseResponse;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.common.ResultUtils;
      import top.panyuwen.gigotapi.common.request.DeleteListRequest;
      import top.panyuwen.gigotapi.common.request.DeleteRequest;
      import top.panyuwen.gigotapi.constant.UserConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.model.dto.userinterfaceinfo.UserInterfaceInfoAddRequest;
      import top.panyuwen.gigotapi.model.dto.userinterfaceinfo.UserInterfaceInfoEditRequest;
      import top.panyuwen.gigotapi.model.dto.userinterfaceinfo.UserInterfaceInfoQueryRequest;
      import top.panyuwen.gigotapi.model.dto.userinterfaceinfo.UserInterfaceInfoUpdateRequest;
      import top.panyuwen.gigotapi.model.entity.User;
      import top.panyuwen.gigotapi.model.entity.UserInterfaceInfo;
      import top.panyuwen.gigotapi.model.vo.UserInterfaceInfoVO;
      import top.panyuwen.gigotapi.service.UserInterfaceInfoService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapiclientsdk.client.GigotApiClient;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.util.List;
      import java.util.stream.Collectors;
      
      /**
       * 接口管理
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @RestController
      @RequestMapping("/userInterfaceInfo")
      @Slf4j
      public class UserInterfaceInfoController {
      
          @Resource
          private UserInterfaceInfoService userInterfaceInfoService;
      
          @Resource
          private UserService userService;
      
          @Autowired
          private GigotApiClient gigotApiClient;
      
          // region 增删改查
      
          /**
           * 创建
           *
           * @param userInterfaceInfoAddRequest
           * @param request
           * @return
           */
          @PostMapping("/add")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Long> addUserInterfaceInfo(@RequestBody UserInterfaceInfoAddRequest userInterfaceInfoAddRequest, HttpServletRequest request) {
              if (userInterfaceInfoAddRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              UserInterfaceInfo userInterfaceInfo = new UserInterfaceInfo();
              BeanUtils.copyProperties(userInterfaceInfoAddRequest, userInterfaceInfo);
              userInterfaceInfoService.validUserInterfaceInfo(userInterfaceInfo, true);
              User loginUser = userService.getLoginUser(request);
              userInterfaceInfo.setUserId(loginUser.getId());
              boolean result = userInterfaceInfoService.save(userInterfaceInfo);
              ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
              long newUserInterfaceInfoId = userInterfaceInfo.getId();
              return ResultUtils.success(newUserInterfaceInfoId);
          }
      
          /**
           * 删除
           *
           * @param deleteRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteById")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> deleteByIdUserInterfaceInfo(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
              if (deleteRequest == null || deleteRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              long id = deleteRequest.getId();
              // 判断是否存在
              UserInterfaceInfo oldUserInterfaceInfo = userInterfaceInfoService.getById(id);
              ThrowUtils.throwIf(oldUserInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可删除
              if (!oldUserInterfaceInfo.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean b = userInterfaceInfoService.removeById(id);
              return ResultUtils.success(b);
          }
      
          /**
           * 批量删除
           *
           * @param deleteListRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteByIds")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> deleteByIdsUserInterfaceInfo(@RequestBody DeleteListRequest deleteListRequest, HttpServletRequest request) {
              if (deleteListRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              List<Long> ids = deleteListRequest.getIds();
              // 判断是否存在
              List<UserInterfaceInfo> userInterfaceInfos = userInterfaceInfoService.listByIds(ids);
              if(userInterfaceInfos.size() != ids.size()){
                  List<Long> idsInDB = userInterfaceInfos.stream().map(UserInterfaceInfo::getId).collect(Collectors.toList());
                  List<Long> differentIds = CollUtil.disjunction(ids, idsInDB).stream().collect(Collectors.toList());
                  log.error("differentIds:{}",differentIds);
                  ThrowUtils.throwIf(differentIds.size() > 0, ErrorCode.NOT_FOUND_ERROR,"未找到删除数据:differentIds:" + differentIds);
              }
              // 校验删除人是本人或者管理员
              List<Long> createUserIds = userInterfaceInfos.stream().map(UserInterfaceInfo::getUserId).distinct().collect(Collectors.toList());
              boolean isNotSelfAndAdmin = (createUserIds.size() > 1 || ! createUserIds.get(0).equals(user.getId())) && !userService.isAdmin(request);
              if (isNotSelfAndAdmin) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR,"只允许管理员或者本人删除");
              }
              boolean b = userInterfaceInfoService.removeByIds(ids);
              return ResultUtils.success(b);
          }
      
          /**
           * 更新(仅管理员)
           *
           * @param userInterfaceInfoUpdateRequest
           * @return
           */
          @PostMapping("/update")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> updateUserInterfaceInfo(@RequestBody UserInterfaceInfoUpdateRequest userInterfaceInfoUpdateRequest) {
              if (userInterfaceInfoUpdateRequest == null || userInterfaceInfoUpdateRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              UserInterfaceInfo userInterfaceInfo = new UserInterfaceInfo();
              BeanUtils.copyProperties(userInterfaceInfoUpdateRequest, userInterfaceInfo);
              // 参数校验
              userInterfaceInfoService.validUserInterfaceInfo(userInterfaceInfo, false);
              long id = userInterfaceInfoUpdateRequest.getId();
              // 判断是否存在
              UserInterfaceInfo oldUserInterfaceInfo = userInterfaceInfoService.getById(id);
              ThrowUtils.throwIf(oldUserInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);
              boolean result = userInterfaceInfoService.updateById(userInterfaceInfo);
              return ResultUtils.success(result);
          }
      
          /**
           * 根据 id 获取
           *
           * @param id
           * @return
           */
          @GetMapping("/get/vo")
          public BaseResponse<UserInterfaceInfoVO> getUserInterfaceInfoVOById(long id, HttpServletRequest request) {
              if (id <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              UserInterfaceInfo userInterfaceInfo = userInterfaceInfoService.getById(id);
              if (userInterfaceInfo == null) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
              }
              return ResultUtils.success(userInterfaceInfoService.getUserInterfaceInfoVO(userInterfaceInfo, request));
          }
      
          /**
           * 分页获取列表(封装类)
           *
           * @param userInterfaceInfoQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/list/page/vo")
          public BaseResponse<Page<UserInterfaceInfoVO>> listUserInterfaceInfoVOByPage(@RequestBody UserInterfaceInfoQueryRequest userInterfaceInfoQueryRequest,
                  HttpServletRequest request) {
              long current = userInterfaceInfoQueryRequest.getCurrent();
              long size = userInterfaceInfoQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              Page<UserInterfaceInfo> userInterfaceInfoPage = userInterfaceInfoService.page(new Page<>(current, size),
                      userInterfaceInfoService.getQueryWrapper(userInterfaceInfoQueryRequest));
              return ResultUtils.success(userInterfaceInfoService.getUserInterfaceInfoVOPage(userInterfaceInfoPage, request));
          }
      
          /**
           * 分页获取当前用户创建的资源列表
           *
           * @param userInterfaceInfoQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/my/list/page/vo")
          public BaseResponse<Page<UserInterfaceInfoVO>> listMyUserInterfaceInfoVOByPage(@RequestBody UserInterfaceInfoQueryRequest userInterfaceInfoQueryRequest,
                  HttpServletRequest request) {
              if (userInterfaceInfoQueryRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User loginUser = userService.getLoginUser(request);
              userInterfaceInfoQueryRequest.setUserId(loginUser.getId());
              long current = userInterfaceInfoQueryRequest.getCurrent();
              long size = userInterfaceInfoQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              Page<UserInterfaceInfo> userInterfaceInfoPage = userInterfaceInfoService.page(new Page<>(current, size),
                      userInterfaceInfoService.getQueryWrapper(userInterfaceInfoQueryRequest));
              return ResultUtils.success(userInterfaceInfoService.getUserInterfaceInfoVOPage(userInterfaceInfoPage, request));
          }
      
          // endregion
      
      //    /**
      //     * 分页搜索(从 ES 查询,封装类)
      //     *
      //     * @param userInterfaceInfoQueryRequest
      //     * @param request
      //     * @return
      //     */
      //    @PostMapping("/search/page/vo")
      //    public BaseResponse<Page<UserInterfaceInfoVO>> searchUserInterfaceInfoVOByPage(@RequestBody UserInterfaceInfoQueryRequest userInterfaceInfoQueryRequest,
      //            HttpServletRequest request) {
      //        long size = userInterfaceInfoQueryRequest.getPageSize();
      //        // 限制爬虫
      //        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
      //        Page<UserInterfaceInfo> userInterfaceInfoPage = userInterfaceInfoService.searchFromEs(userInterfaceInfoQueryRequest);
      //        return ResultUtils.success(userInterfaceInfoService.getUserInterfaceInfoVOPage(userInterfaceInfoPage, request));
      //    }
      
          /**
           * 编辑(用户)
           *
           * @param userInterfaceInfoEditRequest
           * @param request
           * @return
           */
          @PostMapping("/edit")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> editUserInterfaceInfo(@RequestBody UserInterfaceInfoEditRequest userInterfaceInfoEditRequest, HttpServletRequest request) {
              if (userInterfaceInfoEditRequest == null || userInterfaceInfoEditRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              UserInterfaceInfo userInterfaceInfo = new UserInterfaceInfo();
              BeanUtils.copyProperties(userInterfaceInfoEditRequest, userInterfaceInfo);
              // 参数校验
              userInterfaceInfoService.validUserInterfaceInfo(userInterfaceInfo, false);
              User loginUser = userService.getLoginUser(request);
              long id = userInterfaceInfoEditRequest.getId();
              // 判断是否存在
              UserInterfaceInfo oldUserInterfaceInfo = userInterfaceInfoService.getById(id);
              ThrowUtils.throwIf(oldUserInterfaceInfo == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可编辑
              if (!oldUserInterfaceInfo.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean result = userInterfaceInfoService.updateById(userInterfaceInfo);
              return ResultUtils.success(result);
          }
      
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
    • serviceImpl

      package top.panyuwen.gigotapi.service.impl;
      
      import cn.hutool.core.bean.BeanUtil;
      import cn.hutool.core.util.ObjUtil;
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
      import com.google.gson.Gson;
      import org.apache.commons.collections4.CollectionUtils;
      import org.apache.commons.lang3.ObjectUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
      import org.springframework.stereotype.Service;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.constant.CommonConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.mapper.UserInterfaceinfoMapper;
      import top.panyuwen.gigotapi.model.dto.userinterfaceinfo.UserInterfaceInfoQueryRequest;
      import top.panyuwen.gigotapi.model.entity.UserInterfaceInfo;
      import top.panyuwen.gigotapi.model.vo.UserInterfaceInfoVO;
      import top.panyuwen.gigotapi.service.UserInterfaceInfoService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.SqlUtils;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.util.List;
      import java.util.stream.Collectors;
      
      /**
      * @author PYW
      * @description 针对表【user_interfaceinfo(用户接口调用表)】的数据库操作Service实现
      * @createDate 2023-12-29 10:55:22
      */
      @Service
      public class UserInterfaceInfoServiceImpl extends ServiceImpl<UserInterfaceinfoMapper, UserInterfaceInfo>
          implements UserInterfaceInfoService {
      
          private final static Gson GSON = new Gson();
      
          @Resource
          private UserService userService;
      
          @Resource
          private ElasticsearchRestTemplate elasticsearchRestTemplate;
      
          /**
           * 非空判断
           *
           * @param userInterfaceInfo
           * @param add
           */
          @Override
          public void validUserInterfaceInfo(UserInterfaceInfo userInterfaceInfo, boolean add) {
              if (userInterfaceInfo == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              Long id = userInterfaceInfo.getId();
              Long userId = userInterfaceInfo.getUserId();
              Long interfaceId = userInterfaceInfo.getInterfaceId();
              // 创建时,参数不能为空
              if (add) {
                  ThrowUtils.throwIf(ObjUtil.isEmpty(id), ErrorCode.PARAMS_ERROR,"id为空");
                  ThrowUtils.throwIf(ObjUtil.isEmpty(userId), ErrorCode.PARAMS_ERROR,"userId为空");
                  ThrowUtils.throwIf(ObjUtil.isEmpty(interfaceId), ErrorCode.PARAMS_ERROR,"interfaceId为空");
              }
              // 有参数则校验
              if (id <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "id不正确");
              }
              if (userId <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "userId不正确");
              }
              if (interfaceId <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "interfaceId不正确");
              }
          }
      
          /**
           * 获取查询包装类
           *
           * @param userInterfaceInfoQueryRequest
           * @return
           */
          @Override
          public QueryWrapper<UserInterfaceInfo> getQueryWrapper(UserInterfaceInfoQueryRequest userInterfaceInfoQueryRequest) {
              QueryWrapper<UserInterfaceInfo> queryWrapper = new QueryWrapper<>();
              if (userInterfaceInfoQueryRequest == null) {
                  return queryWrapper;
              }
              Long id = userInterfaceInfoQueryRequest.getId();
              Long userId = userInterfaceInfoQueryRequest.getUserId();
              Long interfaceId = userInterfaceInfoQueryRequest.getInterfaceId();
              Integer status = userInterfaceInfoQueryRequest.getStatus();
              String sortField = userInterfaceInfoQueryRequest.getSortField();
              String sortOrder = userInterfaceInfoQueryRequest.getSortOrder();
              queryWrapper.eq(ObjectUtils.isNotEmpty(status), "status", status);
              queryWrapper.eq(ObjectUtils.isNotEmpty(interfaceId), "interfaceId", interfaceId);
              queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
              queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
              queryWrapper.eq("isDelete", false);
              queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                      sortField);
              return queryWrapper;
          }
      
      //    @Override
      //    public Page<UserInterfaceInfo> searchFromEs(UserInterfaceInfoQueryRequest userInterfaceInfoQueryRequest) {
      //        Long id = userInterfaceInfoQueryRequest.getId();
      //        String name = userInterfaceInfoQueryRequest.getName();
      //        String searchText = userInterfaceInfoQueryRequest.getSearchText();
      //        String description = userInterfaceInfoQueryRequest.getDescription();
      //        String url = userInterfaceInfoQueryRequest.getUrl();
      //        String requestHeader = userInterfaceInfoQueryRequest.getRequestHeader();
      //        String responseHeader = userInterfaceInfoQueryRequest.getResponseHeader();
      //        Integer status = userInterfaceInfoQueryRequest.getStatus();
      //        String method = userInterfaceInfoQueryRequest.getMethod();
      //        Long userId = userInterfaceInfoQueryRequest.getUserId();
      //        // es 起始页为 0
      //        long current = userInterfaceInfoQueryRequest.getCurrent();
      //        long pageSize = userInterfaceInfoQueryRequest.getPageSize();
      //        String sortField = userInterfaceInfoQueryRequest.getSortField();
      //        String sortOrder = userInterfaceInfoQueryRequest.getSortOrder();
      //        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
      //        // 过滤
      //        boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
      //        if (id != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
      //        }
      //        if (notId != null) {
      //            boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
      //        }
      //        if (userId != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
      //        }
      //        // 必须包含所有标签
      //        if (CollectionUtils.isNotEmpty(tagList)) {
      //            for (String tag : tagList) {
      //                boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
      //            }
      //        }
      //        // 包含任何一个标签即可
      //        if (CollectionUtils.isNotEmpty(orTagList)) {
      //            BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
      //            for (String tag : orTagList) {
      //                orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
      //            }
      //            orTagBoolQueryBuilder.minimumShouldMatch(1);
      //            boolQueryBuilder.filter(orTagBoolQueryBuilder);
      //        }
      //        // 按关键词检索
      //        if (StringUtils.isNotBlank(searchText)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按标题检索
      //        if (StringUtils.isNotBlank(title)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按内容检索
      //        if (StringUtils.isNotBlank(content)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 排序
      //        SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
      //        if (StringUtils.isNotBlank(sortField)) {
      //            sortBuilder = SortBuilders.fieldSort(sortField);
      //            sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
      //        }
      //        // 分页
      //        PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
      //        // 构造查询
      //        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
      //                .withPageable(pageRequest).withSorts(sortBuilder).build();
      //        SearchHits<UserInterfaceInfoEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, UserInterfaceInfoEsDTO.class);
      //        Page<UserInterfaceInfo> page = new Page<>();
      //        page.setTotal(searchHits.getTotalHits());
      //        List<UserInterfaceInfo> resourceList = new ArrayList<>();
      //        // 查出结果后,从 db 获取最新动态数据(比如点赞数)
      //        if (searchHits.hasSearchHits()) {
      //            List<SearchHit<UserInterfaceInfoEsDTO>> searchHitList = searchHits.getSearchHits();
      //            List<Long> userInterfaceInfoIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
      //                    .collect(Collectors.toList());
      //            List<UserInterfaceInfo> userInterfaceInfoList = baseMapper.selectBatchIds(userInterfaceInfoIdList);
      //            if (userInterfaceInfoList != null) {
      //                Map<Long, List<UserInterfaceInfo>> idUserInterfaceInfoMap = userInterfaceInfoList.stream().collect(Collectors.groupingBy(UserInterfaceInfo::getId));
      //                userInterfaceInfoIdList.forEach(userInterfaceInfoId -> {
      //                    if (idUserInterfaceInfoMap.containsKey(userInterfaceInfoId)) {
      //                        resourceList.add(idUserInterfaceInfoMap.get(userInterfaceInfoId).get(0));
      //                    } else {
      //                        // 从 es 清空 db 已物理删除的数据
      //                        String delete = elasticsearchRestTemplate.delete(String.valueOf(userInterfaceInfoId), UserInterfaceInfoEsDTO.class);
      //                        log.info("delete userInterfaceInfo {}", delete);
      //                    }
      //                });
      //            }
      //        }
      //        page.setRecords(resourceList);
      //        return page;
      //    }
      
          @Override
          public UserInterfaceInfoVO getUserInterfaceInfoVO(UserInterfaceInfo userInterfaceInfo, HttpServletRequest request) {
              // 转为vo对象
              UserInterfaceInfoVO userInterfaceInfoVO = UserInterfaceInfoVO.objToVo(userInterfaceInfo);
              return userInterfaceInfoVO;
          }
      
          @Override
          public Page<UserInterfaceInfoVO> getUserInterfaceInfoVOPage(Page<UserInterfaceInfo> userInterfaceInfoPage, HttpServletRequest request) {
              List<UserInterfaceInfo> userInterfaceInfoList = userInterfaceInfoPage.getRecords();
              Page<UserInterfaceInfoVO> userInterfaceInfoVOPage = new Page<>(userInterfaceInfoPage.getCurrent(), userInterfaceInfoPage.getSize(), userInterfaceInfoPage.getTotal());
              if (CollectionUtils.isEmpty(userInterfaceInfoList)) {
                  return userInterfaceInfoVOPage;
              }
              List<UserInterfaceInfoVO> userInterfaceInfoVOList = userInterfaceInfoList.stream().map(userInterfaceInfo -> {
                  UserInterfaceInfoVO userInterfaceInfoVO = new UserInterfaceInfoVO();
                  BeanUtil.copyProperties(userInterfaceInfo,userInterfaceInfoVO);
                  return userInterfaceInfoVO;
              }).collect(Collectors.toList());
              // 2. 已登录,获取用户点赞、收藏状态
              // 填充信息
              userInterfaceInfoVOPage.setRecords(userInterfaceInfoVOList);
              return userInterfaceInfoVOPage;
          }
      }
      
      
      
      
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236

# GateWay网关

  • 我们开发用户接口关系表的目的是统计接口调用次数等,但是每个接口的开发者可能不一样,那比如说增加次数等功能在每个接口都要写么?为了解决这个问题我们引入GateWay网关
  • 网关教程参考:https://panyw-git.gitee.io/2023/08/01/SpringCloud/?highlight=gateway

# 创建一个新的项目

  • 项目名称:gigotapi-gateway

  • 引入gateway网关

  • 配置网关

    server:
      port: 8000
    spring:
      cloud:
        gateway:
          default-filters:
            - AddResponseHeader=X-Source, gigotapi
          routes:
            - id: route_1
              uri: http://localhost:8002
              predicates:
                - Path=/api/**
    
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 测试:这儿我们可以看到访问8000自动转发到8002接口项目调用获取名称接口了

    image-20240103130528378

# 添加全局过滤器

  • 全局过滤器

    package top.panyuwen.gigotapigateway.gateway;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    /**
     * 自定义全局过滤器
     * @author PYW
     */
    @Slf4j
    @Component
    public class CustomGlobalFilter implements GlobalFilter, Ordered {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("CustomGlobalFilter");
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            // 数字越低越先执行
            return -1;
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
  • 测试成功后开发主要功能

    1. 请求日志

    2. 黑白名单

    3. 用户鉴权

    4. 是否分配给用户

    5. 请求的接口是否存在?

    6. 请求转发,调用接口

    7. 响应日志

      有部分功能需要添加Dubbo和nacos后才能更方便的完成,因此我们代码如下:

      package top.panyuwen.gigotapigateway.gateway;
      
      import lombok.extern.slf4j.Slf4j;
      import org.reactivestreams.Publisher;
      import org.springframework.cloud.gateway.filter.GatewayFilterChain;
      import org.springframework.cloud.gateway.filter.GlobalFilter;
      import org.springframework.core.Ordered;
      import org.springframework.core.io.buffer.DataBuffer;
      import org.springframework.core.io.buffer.DataBufferFactory;
      import org.springframework.core.io.buffer.DataBufferUtils;
      import org.springframework.http.HttpHeaders;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.server.reactive.ServerHttpRequest;
      import org.springframework.http.server.reactive.ServerHttpResponse;
      import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
      import org.springframework.stereotype.Component;
      import org.springframework.web.server.ServerWebExchange;
      import reactor.core.publisher.Flux;
      import reactor.core.publisher.Mono;
      import top.panyuwen.gigotapiclientsdk.utils.SignUtils;
      import top.panyuwen.gigotapigateway.common.ErrorCode;
      import top.panyuwen.gigotapigateway.exception.BusinessException;
      
      import java.io.UnsupportedEncodingException;
      import java.nio.charset.StandardCharsets;
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.List;
      
      /**
       * 自定义全局过滤器
       * @author PYW
       */
      @Slf4j
      @Component
      public class CustomGlobalFilter implements GlobalFilter, Ordered {
      
          /**
           * 黑名单
           */
          private List<String> blackList = Arrays.asList("");
      
          /**
           * 设置执行顺序
           * @return
           */
          @Override
          public int getOrder() {
              // 数字越低越先执行
              return -1;
          }
      
          /**
           * 过滤器执行逻辑
           * @param exchange
           * @param chain
           * @return
           */
          @Override
          public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
              // 1.请求日志
              log.info("请求日志信息:");
              ServerHttpRequest request = exchange.getRequest();
              log.info("请求唯一标识:"  + request.getId());
              log.info("请求路径:" + request.getPath().value());
              log.info("请求方式:" + request.getMethod());
              log.info("请求参数:" + request.getQueryParams());
              String hostString = request.getLocalAddress().getHostString();
              log.info("请求来源地址:" + hostString);
      
              // 2.黑白名单
              if(blackList.contains(hostString)){
                  exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                  return exchange.getResponse().setComplete();
              }
      
              // 3,用户鉴权(判断 ak、sk 是否合法)
              HttpHeaders headers = request.getHeaders();
              String secretId = headers.getFirst("X-SecretId");
              String nonce = headers.getFirst("X-Nonce");
              String body = headers.getFirst("X-Body");
              try {
                  body = new String(body.getBytes("ISO-8859-1"), "UTF-8");
              } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
              }
              String timestamp = headers.getFirst("X-Timestamp");
              String sign = headers.getFirst("X-Sign");
      
              // todo 是否分配给用户校验 后续补齐 这儿为了演示直接给值了
              // todo 校验用户返回的提示不要太精确,不然容易让黑客攻击
              if(!"123456".equals(secretId)){
                  throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥不正确");
              }
      
              // 随机数不得大于一万
              if(Integer.parseInt(nonce) > 10000){
                  throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"随机数不得大于一万");
              }
      
              // 时间和当前时间不得超过五分钟
              if(System.currentTimeMillis() - Long.parseLong(timestamp) > 300000){
                  throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"时间和当前时间不得超过五分钟");
              }
      
              // 解析sign,查询secretKey是否和header中的一致 这儿为了演示secretKey直接给值
              String signInServer = SignUtils.getSign(body, "654321");
              if(!sign.equals(signInServer)){
                  throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"签名不正确");
              }
              // 5.请求的模拟接口是否存在?
              // todo 数据库查询模拟接口是否存在,以及请求方法是否匹配
              // 6.请求转发,调用模拟接口
              // todo 调用成功,接口调用次数 + 1
              // 7.响应日志
              return handleResponse(exchange, chain);
          }
      
          /**
           * 处理响应
           *
           * @param exchange
           * @param chain
           * @return
           *
           */
          public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
              try {
                  // 获取原始的响应对象
                  ServerHttpResponse originalResponse = exchange.getResponse();
                  // 获取数据缓冲工厂
                  DataBufferFactory bufferFactory = originalResponse.bufferFactory();
                  // 获取响应的状态码
                  HttpStatus statusCode = originalResponse.getStatusCode();
      
                  // 判断状态码是否为200 OK(按道理来说,现在没有调用,是拿不到响应码的,对这个保持怀疑 沉思.jpg)
                  if(statusCode == HttpStatus.OK) {
                      // 创建一个装饰后的响应对象(开始穿装备,增强能力)
                      ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
      
                          // 重写writeWith方法,用于处理响应体的数据
                          // 这段方法就是只要当我们的模拟接口调用完成之后,等它返回结果,
                          // 就会调用writeWith方法,我们就能根据响应结果做一些自己的处理
                          @Override
                          public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                              log.info("body instanceof Flux: {}", (body instanceof Flux));
                              // 判断响应体是否是Flux类型
                              if (body instanceof Flux) {
                                  Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                                  // 返回一个处理后的响应体
                                  // (这里就理解为它在拼接字符串,它把缓冲区的数据取出来,一点一点拼接好)
                                  return super.writeWith(fluxBody.map(dataBuffer -> {
                                      // 业务逻辑
                                      // 读取响应体的内容并转换为字节数组
                                      byte[] content = new byte[dataBuffer.readableByteCount()];
                                      dataBuffer.read(content);
                                      DataBufferUtils.release(dataBuffer);//释放掉内存
                                      // 构建响应日志
                                      String data = new String(content, StandardCharsets.UTF_8);//data
                                      // 打印日志
                                      log.info("响应结果:" + data);
                                      // 将处理后的内容重新包装成DataBuffer并返回
                                      return bufferFactory.wrap(content);
                                  }));
                              } else {
                                  log.error("网关处理响应异常", getStatusCode());
                              }
                              return super.writeWith(body);
                          }
                      };
                      // 对于200 OK的请求,将装饰后的响应对象传递给下一个过滤器链,并继续处理(设置repsonse对象为装饰过的)
                      return chain.filter(exchange.mutate().response(decoratedResponse).build());
                  }
                  // 对于非200 OK的请求,直接返回,进行降级处理
                  return chain.filter(exchange);
              }catch (Exception e){
                  // 处理异常情况,记录错误日志
                  log.error("gateway log exception.\n" + e);
                  return chain.filter(exchange);
              }
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183

# 解决跨项目调用方法的问题

# 引入Dubbo和Nacos

  • 引入依赖 backend和gateway都需要添加

            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>3.0.9</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba.nacos</groupId>
                <artifactId>nacos-client</artifactId>
                <version>2.1.0</version>
            </dependency>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 添加配置 backend和gateway都需要添加

    # 以下配置指定了应用的名称、使用的协议(Dubbo)、注册中心的类型(Nacos)和地址
    dubbo:
      application:
        # 设置应用的名称
        name: dubbo-springboot-demo-provider
      # 指定使用 Dubbo 协议,且端口设置为 -1,表示随机分配可用端口
      protocol:
        name: dubbo
        port: 28000
      registry:
        # 配置注册中心为 Nacos,使用的地址是 nacos://localhost:8848
        id: nacos-registry
        address: nacos://localhost:8848
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

# 创建公共服务

  • 名称:gigotapi-common

  • 抽取公共方法和实体类(注:需要把其他项目公共的地方全部抽取过来)

    image-20240105220615358

  • 因为我们的主要业务逻辑是放在后台管理端的gigotapi-backend因此我们在这儿实现这些方法

    • InnerInterfaceInfoServiceImpl

      package top.panyuwen.gigotapi.service.impl.inner;
      
      import cn.hutool.core.util.StrUtil;
      import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
      import org.apache.dubbo.config.annotation.DubboService;
      import org.springframework.beans.factory.annotation.Autowired;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.service.InterfaceInfoService;
      import top.panyuwen.gigotapicommon.model.entity.InterfaceInfo;
      import top.panyuwen.gigotapicommon.service.InnerInterfaceInfoService;
      
      /**
       * @author PYW
       */
      @DubboService
      public class InnerInterfaceInfoServiceImpl implements InnerInterfaceInfoService {
      
          @Autowired
          InterfaceInfoService interfaceInfoService;
      
          @Override
          public InterfaceInfo getInterfaceInfo(String url, String method) {
              if(StrUtil.isBlank(url) || StrUtil.isBlank(method)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              LambdaQueryWrapper<InterfaceInfo> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(InterfaceInfo::getUrl, url).eq(InterfaceInfo::getMethod, method);
              return interfaceInfoService.getOne(queryWrapper);
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
    • InnerUserInterfaceInfoServiceImpl

      package top.panyuwen.gigotapi.service.impl.inner;
      
      import org.apache.dubbo.config.annotation.DubboService;
      import org.springframework.beans.factory.annotation.Autowired;
      import top.panyuwen.gigotapi.service.UserInterfaceInfoService;
      import top.panyuwen.gigotapicommon.service.InnerUserInterfaceInfoService;
      
      /**
       * @author PYW
       */
      @DubboService
      public class InnerUserInterfaceInfoServiceImpl implements InnerUserInterfaceInfoService {
      
          @Autowired
          private UserInterfaceInfoService userInterfaceInfoService;
      
          @Override
          public boolean invokeCount(Long interfaceInfoId, Long userId) {
              return userInterfaceInfoService.invokeCount(interfaceInfoId, userId);
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
    • InnerUserServiceImpl

      package top.panyuwen.gigotapi.service.impl.inner;
      
      import cn.hutool.core.util.StrUtil;
      import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
      import org.apache.dubbo.config.annotation.DubboService;
      import org.springframework.beans.factory.annotation.Autowired;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.service.impl.UserServiceImpl;
      import top.panyuwen.gigotapicommon.model.entity.User;
      import top.panyuwen.gigotapicommon.service.InnerUserService;
      
      /**
       * @author PYW
       */
      @DubboService
      public class InnerUserServiceImpl implements InnerUserService {
      
          @Autowired
          private UserServiceImpl userService;
      
          /**
           * 根据 secretId 查询用户
           * @param secretId
           * @return
           */
          @Override
          public User getInvokeUser(String secretId) {
              // 校验
              if(StrUtil.isBlank(secretId)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              // 封装查询
              LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(User::getSecretId, secretId);
      
              //查询
              return userService.getOne(queryWrapper);
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
  • 这时候我们可以发现nacos已经把这些服务注册上去了

    image-20240105221029959

  • 接着就可以在gateway网关中调用这些方法了

    package top.panyuwen.gigotapigateway.filter;
    
    import cn.hutool.core.util.ObjUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.dubbo.config.annotation.DubboReference;
    import org.apache.dubbo.config.annotation.DubboService;
    import org.reactivestreams.Publisher;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.core.io.buffer.DataBufferFactory;
    import org.springframework.core.io.buffer.DataBufferUtils;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
    import org.springframework.stereotype.Component;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    import top.panyuwen.gigotapiclientsdk.utils.SignUtils;
    import top.panyuwen.gigotapicommon.model.entity.InterfaceInfo;
    import top.panyuwen.gigotapicommon.model.entity.User;
    import top.panyuwen.gigotapicommon.service.InnerInterfaceInfoService;
    import top.panyuwen.gigotapicommon.service.InnerUserInterfaceInfoService;
    import top.panyuwen.gigotapicommon.service.InnerUserService;
    import top.panyuwen.gigotapigateway.common.ErrorCode;
    import top.panyuwen.gigotapigateway.exception.BusinessException;
    
    import java.io.UnsupportedEncodingException;
    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * 自定义全局过滤器
     * @author PYW
     */
    @Slf4j
    @Component
    public class CustomGlobalFilter implements GlobalFilter, Ordered {
    
        @DubboReference
        private InnerUserService innerUserService;
    
        @DubboReference
        private InnerUserInterfaceInfoService innerUserInterfaceInfoService;
    
        @DubboReference
        private InnerInterfaceInfoService innerInterfaceInfoService;
    
        /**
         * 黑名单
         */
        private List<String> blackList = Arrays.asList("");
    
        /**
         * 设置执行顺序
         * @return
         */
        @Override
        public int getOrder() {
            // 数字越低越先执行
            return -1;
        }
    
        /**
         * 过滤器执行逻辑
         * @param exchange
         * @param chain
         * @return
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.请求日志
            log.info("GateWay网关全局过滤器请求日志信息==========>");
            ServerHttpRequest request = exchange.getRequest();
            // /api/name/user
            String path = request.getPath().value();
            String method = request.getMethod().toString();
            String url = request.getURI().toString().trim();
            log.info("请求唯一标识:"  + request.getId());
            log.info("请求路径:" + path);
            log.info("请求完整url:" + url);
            log.info("请求方式:" + method);
            String hostString = request.getLocalAddress().getHostString();
            log.info("请求来源地址:" + hostString);
    
            // 2.黑白名单
            if(blackList.contains(hostString)){
                exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                return exchange.getResponse().setComplete();
            }
    
            // 3,用户鉴权(判断 ak、sk 是否合法)
            HttpHeaders headers = request.getHeaders();
            String secretId = headers.getFirst("X-SecretId");
            String nonce = headers.getFirst("X-Nonce");
            String body = headers.getFirst("X-Body");
            try {
                body = new String(body.getBytes("ISO-8859-1"), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            String timestamp = headers.getFirst("X-Timestamp");
            String sign = headers.getFirst("X-Sign");
    
            // Api密钥是否分配给用户校验
            User invokeUser = null;
            try{
                invokeUser = innerUserService.getInvokeUser(secretId);
                if(ObjUtil.isEmpty(invokeUser)){
                    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"请求用户密钥不存在");
                }
                // todo 校验用户金币是否为0
            }catch (Exception e){
                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"获取Invoke异常");
            }
            if(invokeUser == null){
                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"未查询到用户信息");
            }
    
            if(!invokeUser.getSecretId().equals(secretId)){
                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
            }
    
            // 随机数不得大于一万
            if(Integer.parseInt(nonce) > 10000){
                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
            }
    
            // 防重发,时间和当前时间不得超过五分钟
            if(System.currentTimeMillis() - Long.parseLong(timestamp) > 300000){
                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
            }
    
            // 解析sign,查询secretKey是否和header中的一致
            String secretKeyInServer = invokeUser.getSecretKey();
            String signInServer = SignUtils.getSign(body,secretKeyInServer);
            // 检查请求中的签名是否为空,或者是否与服务器的签名不一致
            if(sign == null || !sign.equals(signInServer)){
                throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
            }
            // 5.请求的模拟接口是否存在?
            if("GET".equals(method)){
                MultiValueMap<String, String> queryParams = request.getQueryParams();
                log.info("GET请求参数:" + queryParams);
            } else if("POST".equals(method)){
                // 获取post请求的参数
                log.info("POST请求参数:");
            }
            // todo 数据库查询模拟接口是否存在,以及请求方法是否匹配
            InterfaceInfo interfaceInfo = null;
            try{
                interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(url, method);
                if(ObjUtil.isEmpty(interfaceInfo)){
                    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"请求接口不存在");
                }
            }catch (Exception e){
                log.error("获取接口信息失败", e.getMessage());
            }
            // 6.请求转发,调用模拟接口
            // 7.响应日志
            return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());
        }
    
        /**
         * 处理响应
         *
         * @param exchange
         * @param chain
         * @return
         *
         */
        public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain,Long interfaceId, Long userId) {
            try {
                // 获取原始的响应对象
                ServerHttpResponse originalResponse = exchange.getResponse();
                // 获取数据缓冲工厂
                DataBufferFactory bufferFactory = originalResponse.bufferFactory();
                // 获取响应的状态码
                HttpStatus statusCode = originalResponse.getStatusCode();
    
                // 判断状态码是否为200 OK(按道理来说,现在没有调用,是拿不到响应码的,对这个保持怀疑 沉思.jpg)
                if(statusCode == HttpStatus.OK) {
                    // 创建一个装饰后的响应对象(开始穿装备,增强能力)
                    ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
    
                        // 重写writeWith方法,用于处理响应体的数据
                        // 这段方法就是只要当我们的模拟接口调用完成之后,等它返回结果,
                        // 就会调用writeWith方法,我们就能根据响应结果做一些自己的处理
                        @Override
                        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                            log.info("响应体是否是Flux类型: {}", (body instanceof Flux));
                            // 判断响应体是否是Flux类型
                            if (body instanceof Flux) {
                                Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                                // 返回一个处理后的响应体
                                // (这里就理解为它在拼接字符串,它把缓冲区的数据取出来,一点一点拼接好)
                                return super.writeWith(fluxBody.map(dataBuffer -> {
                                    // 调用成功后的处理逻辑
                                    // 调用成功,接口调用次数+1
                                    try {
                                        // 记录接口调用次数
                                        innerUserInterfaceInfoService.invokeCount(interfaceId,userId);
                                    } catch (Exception e) {
                                        log.error("调用成功后的处理逻辑异常", e.getMessage());
                                    }
                                    // 读取响应体的内容并转换为字节数组
                                    byte[] content = new byte[dataBuffer.readableByteCount()];
                                    dataBuffer.read(content);
                                    DataBufferUtils.release(dataBuffer);//释放掉内存
                                    // 构建响应日志
                                    String data = new String(content, StandardCharsets.UTF_8);//data
                                    // 打印日志
                                    log.info("响应结果:" + data);
                                    // 将处理后的内容重新包装成DataBuffer并返回
                                    return bufferFactory.wrap(content);
                                }));
                            } else {
                                log.error("网关处理响应异常", getStatusCode());
                            }
                            return super.writeWith(body);
                        }
                    };
                    // 对于200 OK的请求,将装饰后的响应对象传递给下一个过滤器链,并继续处理(设置repsonse对象为装饰过的)
                    return chain.filter(exchange.mutate().response(decoratedResponse).build());
                }
                // 对于非200 OK的请求,直接返回,进行降级处理
                return chain.filter(exchange);
            }catch (Exception e){
                // 处理异常情况,记录错误日志
                log.error("gateway log exception.\n" + e);
                return chain.filter(exchange);
            }
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242

# 优化SDK

  1. SDK目前没有统一调用的方法,我们backend项目访问时只能访问固定方法,而现在我想根据用户访问的api地址去解析

  2. 统一http发送请求(POST,GET)

  3. 引入gitgotApiException,用于统一给调用该sdk的用户抛出错误

    
    package top.panyuwen.gigotapiclientsdk.client;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.bean.copier.CopyOptions;
    import cn.hutool.core.util.ObjUtil;
    import cn.hutool.core.util.RandomUtil;
    import cn.hutool.core.util.StrUtil;
    import cn.hutool.http.HttpRequest;
    import cn.hutool.http.HttpResponse;
    import com.alibaba.fastjson.JSON;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import top.panyuwen.gigotapiclientsdk.exception.ErrorCode;
    import top.panyuwen.gigotapiclientsdk.exception.GigotApiException;
    import top.panyuwen.gigotapiclientsdk.model.BaseRequest;
    import top.panyuwen.gigotapiclientsdk.model.entity.User;
    import top.panyuwen.gigotapiclientsdk.model.enums.UrlToMethodEnum;
    import top.panyuwen.gigotapiclientsdk.model.response.PoisonousChickenSoupResponse;
    import top.panyuwen.gigotapiclientsdk.model.response.UserResponse;
    import top.panyuwen.gigotapiclientsdk.utils.SignUtils;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 调用第三方客户端接口
     *
     * @author PYW
     */
    @Slf4j
    @Data
    public class GigotApiClient {
    
        private String secretId;
    
        private String secretKey;
    
        private static final String GATEWAY_URL = "http://localhost:8000";
    
        public GigotApiClient() {
        }
    
        public GigotApiClient(String secretId, String secretKey) {
            this.secretId = secretId;
            this.secretKey = secretKey;
        }
    
        /**
         * 通过POST请求获取用户名
         *
         * @param user 用户信息
         * @return 用户名
         */
        public UserResponse getUsernameByPost(User user) throws GigotApiException {
    
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.name.getPath(), "POST", user, UserResponse.class);
        }
    
        /**
         * 通过GET请求获取毒鸡汤
         *
         * @return 毒鸡汤
         */
        public PoisonousChickenSoupResponse getPoisonousChickenSoup() throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.poisonousChickenSoupResponse.getPath(), "GET", null, PoisonousChickenSoupResponse.class);
        }
    
    
        /**
         * 解析地址和调用接口
         *
         * @param baseRequest
         * @return
         * @throws GigotApiException
         */
        public Object parseAddressAndCallInterface(BaseRequest baseRequest) throws GigotApiException {
            String path = baseRequest.getPath();
            String method = baseRequest.getMethod();
            Map<String, Object> paramsList = baseRequest.getRequestParams();
            Class<?> clazz = GigotApiClient.class;
            Object result = null;
            try {
                log.info("请求地址:{},请求方法:{},请求参数:{}", path, method, paramsList);
                if (path.equals(UrlToMethodEnum.name.getPath())) {
                    return invokeMethod(UrlToMethodEnum.name.getMethod(), paramsList, User.class);
                } else if (path.equals(UrlToMethodEnum.poisonousChickenSoupResponse.getPath())) {
                    return invokeMethod(UrlToMethodEnum.poisonousChickenSoupResponse.getMethod());
                }
            } catch (Exception e) {
                throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "未找到该方法");
            }
            if (ObjUtil.isEmpty(result)) {
                throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "未找到Url对应资源");
            }
            log.info("返回结果:{}", result);
            return result;
        }
    
    
        /**
         * 反射方法(不带参数)
         *
         * @param methodName
         * @return
         * @throws GigotApiException
         */
        private Object invokeMethod(String methodName) throws GigotApiException {
            return this.invokeMethod(methodName, null, null);
        }
    
        /**
         * 反射方法(带参)
         *
         * @param methodName
         * @param params
         * @param paramsType
         * @return
         * @throws GigotApiException
         */
        private Object invokeMethod(String methodName, Map<String, Object> params, Class<?> paramsType) throws GigotApiException {
            try {
                Class<?> clazz = GigotApiClient.class;
                if (params == null) {
                    Method method = clazz.getMethod(methodName);
                    return method.invoke(this);
                } else {
                    log.info("接收到的参数 params:{} paramsType:{}",params,paramsType);
                    Method method = clazz.getMethod(methodName, paramsType);
                    // map转Object
                    Object paramsObject = BeanUtil.mapToBean(params, paramsType, true, CopyOptions.create());
                    log.info("map转Object params:{} paramsType:{}",params,paramsType);
                    return method.invoke(this, paramsObject);
                }
            } catch (Exception e) {
                throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "调用方法失败");
            }
        }
    
    
        /**
         * 发送请求
         *
         * @param url          请求地址
         * @param method       请求方式
         * @param params       请求参数
         * @param responseType 响应类型
         * @param <T>          响应类型
         * @return 响应结果
         * @throws GigotApiException 异常
         */
        public <T> T sendRequest(String url, String method, Object params, Class<T> responseType) throws GigotApiException {
            HttpRequest request;
            String jsonBody = JSON.toJSONString(params);
    
            switch (method) {
                case "GET":
                    log.info("params:{}", params);
                    request = HttpRequest.get(url);
                    handleParamsAsQueryParams(request, params);
                    break;
                case "POST":
                    log.info("封装body:{}", jsonBody);
                    request = HttpRequest.post(url);
                    handleParamsAsBody(request, jsonBody);
                    break;
                // Add more cases for other HTTP methods if needed
                default:
                    throw new GigotApiException(ErrorCode.PARAMS_ERROR, "请求方式有误");
            }
    
            // 添加通用请求头
            request.addHeaders(getHeaderMap(jsonBody));
            HttpResponse response = request.execute();
            String responseBody = response.body();
            log.info("responseBody:{}", responseBody);
            // Convert the response body to the specified type using JSON deserialization
            return JSON.parseObject(responseBody, responseType);
        }
    
        private void handleParamsAsQueryParams(HttpRequest request, Object bodyAndParams) {
            if (bodyAndParams != null) {
                Map<String, Object> paramsMap = convertObjectToMap(bodyAndParams);
                for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
                    if (entry.getValue() != null) {
                        request.form(entry.getKey(), entry.getValue().toString());
                    }
                }
            }
        }
    
        private void handleParamsAsBody(HttpRequest request, String bodyJson) {
            if (bodyJson != null) {
                // Convert the entire bodyAndParams map to JSON and set it as the request body
                request.header("Content-Type", "application/json; charset=UTF-8")
                        .body(bodyJson);
            }
        }
    
        private Map<String, Object> convertObjectToMap(Object object) {
            // Use reflection to get all fields and their values from the object
            Map<String, Object> map = new HashMap<>();
            Field[] fields = object.getClass().getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                try {
                    map.put(field.getName(), field.get(object));
                } catch (IllegalAccessException e) {
                    // Handle the exception as needed
                }
            }
            return map;
        }
    
        /**
         * 检查配置
         *
         * @param gigotApiClient api客户端
         * @throws GigotApiException 业务异常
         */
        public void checkConfig(GigotApiClient gigotApiClient) throws GigotApiException {
            if (StrUtil.isBlank(gigotApiClient.getSecretId()) && StrUtil.isBlank(gigotApiClient.getSecretKey())) {
                throw new GigotApiException(ErrorCode.NO_AUTH_ERROR, "请先配置密钥AccessKey/SecretKey");
            }
        }
    
        /**
         * 获取请求头Map
         *
         * @param body 请求体
         * @return 请求头Map
         */
        private Map<String, String> getHeaderMap(String body) {
            Map<String, String> headerMap = new HashMap<>();
            headerMap.put("X-SecretId", secretId);
            // 一定不能发送给后端, 后端会自动计算签名
    //        headerMap.put("X-SecretKey", secretKey);
            headerMap.put("X-Nonce", RandomUtil.randomNumbers(4));
            headerMap.put("X-Body", body);
            headerMap.put("X-Timestamp", System.currentTimeMillis() + "");
            headerMap.put("X-Sign", SignUtils.getSign(body, secretKey));
            return headerMap;
        }
    
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248

# 引入Redis

上面我们的主要业务流程就开发完毕了,现在要引入redis存储数据,我们需要修改以下高频访问的模块

想了想后面要部署到云服务器上面,所以还是直接在云服务器上安装个docker在容器里面安装redis然后可以远程访问就行了,部署docker安装redis参考:https://panyw-git.gitee.io/2023/09/07/Centos7%E5%AE%89%E8%A3%85Docker/?highlight=docker

# 修改模块

  1. 登录/注册
  2. 用户信息查询
  3. 接口信息查询
  4. 删除项目通用模板中无用的类(如POSTService)
  5. 部分功能优化

# 开发流程

  1. 添加redis配置

      redis:
        database: 1
        host: 自己填写
        port: 自己填写
        timeout: 5000
        password: 自己填写
    
    1
    2
    3
    4
    5
    6
  2. 引入我们自己编写的redis工具类

    package com.hmdp.utils;
    
    import cn.hutool.core.util.BooleanUtil;
    import cn.hutool.core.util.StrUtil;
    import com.alibaba.fastjson.JSON;
    import com.baomidou.mybatisplus.extension.api.R;
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.hmdp.entity.Shop;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.time.LocalDateTime;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Function;
    
    import static com.hmdp.utils.RedisConstants.*;
    
    /**
     * 基于StringRedisTemplate封装缓存工具类
     *
     * 需求:
     * 方法1:将任意java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
     * 方法2:将任意java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
     * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
     * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
     */
    @Slf4j
    @Component
    public class CacheClient {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
    
    
        /**
         *  将任意java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
         * @param key
         * @param value
         * @param timeout
         * @param unit
         */
        public void set(String key, Object value, Long timeout, TimeUnit unit){
            stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(value),timeout,unit);
        }
    
        /**
         * 将任意java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
         * @param key
         * @param value
         * @param timeout
         * @param unit
         */
        public void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit){
            // 设置逻辑过期
            RedisData redisData = new RedisData();
            redisData.setData(value);
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout)));
            // 写入redis
            stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(redisData));
        }
    
        /**
         * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
         * @param keyPrefix
         * @param id
         * @param type
         * @param timeout
         * @param unit
         * @param dbFallback
         * @param <R>
         * @param <ID>
         * @return
         */
        public <R,ID> R getWithPassThrough(String keyPrefix, ID id, Class<R> type,Long timeout, TimeUnit unit, Function<ID,R> dbFallback){
            String key = keyPrefix + id;
            //1.从redis查询商铺缓存
            String json = stringRedisTemplate.opsForValue().get(key);
            //2.判断缓存是否命中
            /*
                StrUtil.isNotBlank
                    形参为null false
                    形参为"" false
                    形参为"\t\n" false
                    形参为"abc" true
             */
            if (StrUtil.isNotBlank(json)) {
                //3.命中,返回商铺信息
                return JSON.parseObject(json, type);
            }
            //判断是否是空值
            if(json != null){
                // 返回错误信息
                return null;
            }
            //4.未命中,根据id查询数据库
            R r = dbFallback.apply(id);
    
            if (r == null) {
                //5.不存在放回404
                //解决缓存穿透问题,向redis插入空值
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //6.将商铺写入redis,返回商铺信息
            this.set(key,r,timeout,unit);
            return r;
        }
    
        //开启线程池
        private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    
        //锁过期时间 对外提供getset方法,默认10s
        private Long lockTimeout = 10L;
        private TimeUnit lockUnit = TimeUnit.SECONDS;
        public void setLockTimeout(Long lockTimeout,TimeUnit lockUnit) {
            this.lockTimeout = lockTimeout;
            this.lockUnit = lockUnit;
        }
    
        /**
         * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
         * @param keyPrefix key前缀
         * @param id 标识(注:如果方法中参数id这个地方就传id)
         * @param type 返回值类型
         * @param timeout 过期时间
         * @param unit 过期时间单位
         * @param dbfailback 如果未获取到缓存中的数据执行的方法
         * @param <R>
         * @param <ID>
         * @return 查询结果
         */
        public <R,ID> R getWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Long timeout,TimeUnit unit,Function<ID,R> dbfailback) {
            String key = keyPrefix + id;
            //1.从redis查询商铺缓存
            String json = stringRedisTemplate.opsForValue().get(key);
            //2.判断缓存是否命中
            if (StrUtil.isBlank(json)) {
                //3.未命中,直接返回
                log.debug("未命中,开始判断redis中是否有\"\"数据");
                //检查是否存在redis但是redis数据为"" 说明用户已经请求过,且查询过数据库没有这个值,解决缓存穿透问题
                if(json == ""){
                    log.debug("\"\"数据,直接返回null不走数据库");
                    return null;
                }
                log.debug("redis中不存在这个数据,查询数据库");
                //查询数据库是否有这个数据
                R rMiss = dbfailback.apply(id);
    
                if(rMiss == null){
                    log.debug("数据库查询结果为null,存入\"\"数据到redis");
                    //解决缓存穿透问题,向redis插入空值
                    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                    return null;
                }
                log.debug("数据库查询结果不为null,存入数据到redis,并返回结果");
                //存入redis
                setWithLogicalExpire(key,rMiss,timeout,unit);
                //返回数据
                return rMiss;
            }
            //4.命中,需要把json反序列化为对象
            RedisData shopRedisData = JSON.parseObject(json, RedisData.class);
            LocalDateTime shopExpireTime = shopRedisData.getExpireTime();
            R r = JSON.parseObject(shopRedisData.getData().toString(),type);
            //5.判断是否过期
            if(LocalDateTime.now().isBefore(shopExpireTime)){
                //5.1未过期,直接返回店铺信息
                return r;
            }
            //5.2过期,需要缓存重建
            //6.缓存重建
            //6.1获取互斥锁
            String lockKey = LOCK_SHOP_KEY + id;
            boolean isLock = tryLock(lockKey);
            //6.2判断是否获取锁成功
            if(isLock){
                log.debug("获取锁成功!");
                //6.3成功
                //Redis doubleCheck 重新检查缓存,可能在获取锁之前其他线程已经将数据放入缓存
                //"Double Check" 是指在查询缓存之前,首先进行一次检查,看看数据是否存在于缓存中
                // 如果存在,则直接返回缓存数据。
                // 如果不存在,再进一步进行查询数据库的操作,并在查询到数据后,将数据存入缓存中,以供下一次查询使用。
                RedisData redisDataDoubleCheck = JSON.parseObject(stringRedisTemplate.opsForValue().get(key), RedisData.class);
                LocalDateTime expireTimeDoubleCheck = redisDataDoubleCheck.getExpireTime();
                if (LocalDateTime.now().isBefore(expireTimeDoubleCheck)) {
                    //3.未过期,直接返回
                    R rDoubleCheck = JSON.parseObject(shopRedisData.getData().toString(),type);
                    log.debug("DoubleCheck未过期返回shop:{}",rDoubleCheck);
                    return rDoubleCheck;
                }
                //过期,开启独立线程,实现缓存重建
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    //重建缓存
                    //实际开发中应该设置30分钟,这个地方只设置20s方便测试
                    try {
                        //存入数据库
                        R r1 = dbfailback.apply(id);
                        //写入redis
                        setWithLogicalExpire(key,r1,timeout,unit);
                        log.debug("重建缓存成功!");
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        // 释放锁
                        unlock(lockKey);
                    }
                });
            }
    
            //6.4获取锁失败,返回旧的店铺信息
            log.debug("获取锁失败!");
            return r;
        }
    
        /**
         * 获取锁
         * @param key
         * @return false:锁被占用获取失败 true:锁没被占用
         */
        private boolean tryLock(String key){
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", lockTimeout, lockUnit);
            //需要转换为基本数据类型
            //拆箱可能会有空指针异常,所以使用糊涂包的工具类拆箱
            return BooleanUtil.isTrue(flag);
        }
    
        /**
         * 解锁
         * @param key
         */
        private void unlock(String key){
            stringRedisTemplate.delete(key);
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240

# 提供更多的api接口

提供了更多的api接口,用于用户调用,并且统一返回封装在sdk中的对象,方便用户调用

package top.panyuwen.gigotapiinterface.controller;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.URLUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.CityResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import top.panyuwen.gigotapiclientsdk.model.entity.User;
import top.panyuwen.gigotapiclientsdk.model.params.HoroscopeParams;
import top.panyuwen.gigotapiclientsdk.model.params.PublicIpParams;
import top.panyuwen.gigotapiclientsdk.model.params.WebFaviconIconParams;
import top.panyuwen.gigotapiclientsdk.model.response.*;
import org.springframework.core.io.ClassPathResource;


import top.panyuwen.gigotapiinterface.utils.RequestUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 查询名称 api
 * @author PYW
 */
@RestController
@RequestMapping("/")
@Slf4j
public class InterfaceController {

    private final DatabaseReader dbReader;

    public InterfaceController() throws IOException {
        log.info("正在加载GeoIP2 数据库文件......");
        // 加载 GeoIP2 数据库文件
        InputStream databaseStream = new ClassPathResource("GeoLite2-city.mmdb").getInputStream();
        log.info("加载文件成功");
        // 初始化数据库阅读器
        this.dbReader = new DatabaseReader.Builder(databaseStream).build();
    }

    @GetMapping()
    public String getNameByGet(String name){
        return "Get 你的名字是" + name;
    }

    @PostMapping()
    public String getNameByPost(@RequestParam String name){
        return "Post 你的名字是" + name;
    }

    /**
     * 通过POST请求获取用户名
     * @param user 用户信息
     * @param request HTTP请求对象
     * @return 用户名
     */
    @PostMapping("/name")
    public UserResponse getUsernameByPost(@RequestBody User user, HttpServletRequest request){
        log.info("访问成功 user:{}" , user);
        UserResponse userResponse = BeanUtil.copyProperties(user, UserResponse.class);
        return userResponse;
    }

    /**
     * 获取毒鸡汤
     * @return 返回毒鸡汤
     */
    @GetMapping("/poisonousChickenSoup")
    public PoisonousChickenSoupResponse getPoisonousChickenSoup(){
        String poisonousChickenSoupJson = RequestUtils.get("https://api.btstu.cn/yan/api.php?charset=utf-8&encode=json");
        PoisonousChickenSoupResponse poisonousChickenSoupResponse = JSON.parseObject(poisonousChickenSoupJson, PoisonousChickenSoupResponse.class);
        return poisonousChickenSoupResponse;
    }

    /**
     * 获取微博热搜
     * @return 返回微博热搜
     */
    @GetMapping("/weiboHotSearch")
    public WeiboHotSearchResponse getWeiboHotSearch(){
        // 1. 访问微博热搜接口
        String responseJson = RequestUtils.get("https://weibo.com/ajax/side/hotSearch");
        // 解析 JSON
        JSONObject jsonObject = JSONObject.parseObject(responseJson);

        // 获取微博的realtime数组
        JSONArray realtimeArray = jsonObject.getJSONObject("data").getJSONArray("realtime");
        // 遍历realtime数组并只保留note、label_name和num字段

        List<WeiboHot> weiboHotList = new ArrayList<>();
        for (int i = 0; i < realtimeArray.size(); i++) {
            JSONObject realtimeObject = realtimeArray.getJSONObject(i);
            JSONObject filteredObject = new JSONObject();
            String note = realtimeObject.getString("note");
            filteredObject.put("index", i+1);
            filteredObject.put("title", note);
            filteredObject.put("hotType", realtimeObject.getString("label_name"));
            filteredObject.put("hotNum", realtimeObject.getInteger("num"));
            filteredObject.put("url", "https://s.weibo.com/weibo?q=%23"+ URLUtil.encode(note) +"%23");
            WeiboHot weiboHot = filteredObject.toJavaObject(WeiboHot.class);
            weiboHotList.add(weiboHot);
        }
        WeiboHotSearchResponse weiboHotSearchResponse = new WeiboHotSearchResponse();
        weiboHotSearchResponse.setWeibohotSearch(weiboHotList);

        // 3.返回
        return weiboHotSearchResponse;
    }

    /**
     * 获取星座运势
     * @return 返回星座运势
     */
    @PostMapping("/horoscope")
    public HoroscopeResponse getHoroscope(@RequestBody HoroscopeParams horoscope) {
        String type = horoscope.getType();
        String time = horoscope.getTime();
        String responseJson = RequestUtils.get("https://api.vvhan.com/api/horoscope?type=" + type + "&time=" + time);
        // 2. 处理返回结果
        JSONObject data = JSONObject.parseObject(responseJson).getJSONObject("data");
        HoroscopeResponse horoscopeResponse = data.toJavaObject(HoroscopeResponse.class);
        return horoscopeResponse;
    }

    /**
     * 获取用户公网ip地址和城市信息
     */
    @PostMapping ("/publicIp")
    public PublicIpResponse getPublicIp(@RequestBody PublicIpParams publicIp) {
        String ipAddress = publicIp.getIpAddress();

        // 解析 IP 地址并获取城市信息
        CityResponse cityResponse = null;
        String cityName = "";
        String countryName = "";
        PublicIpResponse publicIpResponse = new PublicIpResponse();

        try {
            InetAddress inetAddress = InetAddress.getByName(ipAddress);
            cityResponse = dbReader.city(inetAddress);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (GeoIp2Exception e) {
            log.info("未查询到ip地址");
            // 从 CityResponse 中提取城市信息
            // 构造响应数据
            publicIpResponse.setIpAddress(ipAddress);
            publicIpResponse.setCity(cityName);
            publicIpResponse.setCountry(countryName);
            // 将 Map 转换为 JSON 字符串
            return publicIpResponse;
        }

        // 从 CityResponse 中提取城市信息
        cityName = cityResponse.getCity().getName();
        countryName = cityResponse.getCountry().getName();

        // 构造响应数据
        publicIpResponse.setIpAddress(ipAddress);
        publicIpResponse.setCity(cityName);
        publicIpResponse.setCountry(countryName);
        // 返回结果
        return publicIpResponse;
    }

    /**
     * 获取随机壁纸
     * @return
     */
    @GetMapping("/randomWallpaper")
    public RandomWallpaperResponse getRandomWallpaper(){
        String wallpaperJson = RequestUtils.get("https://btstu.cn/sjbz/api.php?lx=dongman&format=json");
        return JSONObject.parseObject(wallpaperJson).toJavaObject(RandomWallpaperResponse.class);
    }

    /**
     * 获取网站图标
     * @return
     */
    @GetMapping("/webFaviconIcon")
    public String getWebFaviconIcon(WebFaviconIconParams webFaviconIcon){
        String url = webFaviconIcon.getUrl();
        // todo 响应为图片处理
        return RequestUtils.get("https://btstu.cn/getfav/api.php?url="+url);
    }

    /**
     * 土味情话
     * @return
     */
    @GetMapping("/loveTalk")
    public LoveTalkResponse getLoveTalk() {
        String loveTalk = RequestUtils.get("https://api.vvhan.com/api/love");
        LoveTalkResponse loveTalkResponse = new LoveTalkResponse();
        loveTalkResponse.setText(loveTalk);
        return loveTalkResponse;
    }

    /**
     * 天气信息
     * @return
     */
    @GetMapping("/weather")
    public WeatherResponse getWeather() {
        String weatherJson = RequestUtils.get("https://api.vvhan.com/api/weather");
        return JSON.parseObject(weatherJson, WeatherResponse.class);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

# 优化SDK

  1. 添加接口方法和实体类

    • 请求参数命名规则:接口名+Params

    • 响应参数命名规则:接口名+Response

    • 新增UrlToMethodEnum地址转换方法枚举统一url对应方法名

    • package top.panyuwen.gigotapiclientsdk.model.enums;
      
      import org.springframework.util.ObjectUtils;
      
      import java.util.Arrays;
      import java.util.List;
      import java.util.stream.Collectors;
      
      /**
       * 接口信息状态枚举
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      public enum UrlToMethodEnum {
      
          name("/api/name", "getUsernameByPost"),
          poisonousChickenSoup("/api/poisonousChickenSoup", "getPoisonousChickenSoup"),
          weiboHotSearch("/api/weiboHotSearch", "getWeiboHotSearch"),
          horoscope("/api/horoscope", "getHoroscope"),
          publicIp("/api/publicIp", "getPublicIp"),
          randomWallpaper("/api/randomWallpaper", "getRandomWallpaper"),
          webFaviconIcon("/api/webFaviconIcon", "getWebFaviconIcon"),
          loveTalk("/api/loveTalk", "getLoveTalk"),
          weather("/api/weather", "getWeather");
      
          private final String path;
      
          private final String method;
      
          UrlToMethodEnum(String path, String method) {
              this.path = path;
              this.method = method;
          }
      
          /**
           * 获取值列表
           *
           * @return
           */
          public static List<String> getValues() {
              return Arrays.stream(values()).map(item -> item.method).collect(Collectors.toList());
          }
      
          /**
           * 根据 value 获取枚举
           *
           * @param value
           * @return
           */
          public static UrlToMethodEnum getEnumByValue(String value) {
              if (ObjectUtils.isEmpty(value)) {
                  return null;
              }
              for (UrlToMethodEnum anEnum : UrlToMethodEnum.values()) {
                  if (anEnum.method.equals(value)) {
                      return anEnum;
                  }
              }
              return null;
          }
      
          public String getMethod() {
              return method;
          }
      
          public String getPath() {
              return path;
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
  2. 优化sdk

    • 响应数据统一使用json格式接收并返回响应的Response实体类
    
    package top.panyuwen.gigotapiclientsdk.client;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.bean.copier.CopyOptions;
    import cn.hutool.core.util.ObjUtil;
    import cn.hutool.core.util.RandomUtil;
    import cn.hutool.core.util.StrUtil;
    import cn.hutool.http.HttpRequest;
    import cn.hutool.http.HttpResponse;
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.TypeReference;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import top.panyuwen.gigotapiclientsdk.exception.ErrorCode;
    import top.panyuwen.gigotapiclientsdk.exception.GigotApiException;
    import top.panyuwen.gigotapiclientsdk.model.BaseRequest;
    import top.panyuwen.gigotapiclientsdk.model.entity.User;
    import top.panyuwen.gigotapiclientsdk.model.enums.UrlToMethodEnum;
    import top.panyuwen.gigotapiclientsdk.model.params.HoroscopeParams;
    import top.panyuwen.gigotapiclientsdk.model.params.PublicIpParams;
    import top.panyuwen.gigotapiclientsdk.model.response.*;
    import top.panyuwen.gigotapiclientsdk.utils.SignUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * 调用第三方客户端接口
     *
     * @author PYW
     */
    @Slf4j
    @Data
    public class GigotApiClient {
    
        private String secretId;
    
        private String secretKey;
    
        private static final String GATEWAY_URL = "http://localhost:8000";
    
        public GigotApiClient() {
        }
    
        public GigotApiClient(String secretId, String secretKey) {
            this.secretId = secretId;
            this.secretKey = secretKey;
        }
    
        /**
         * 通过POST请求获取用户名
         *
         * @param user 用户信息
         * @return 用户名
         */
        public UserResponse getUsernameByPost(User user) throws GigotApiException {
    
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.name.getPath(), "POST", user, UserResponse.class);
        }
    
        /**
         * 通过GET请求获取毒鸡汤
         *
         * @return 毒鸡汤
         */
        public PoisonousChickenSoupResponse getPoisonousChickenSoup() throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.poisonousChickenSoup.getPath(), "GET", null, PoisonousChickenSoupResponse.class);
        }
    
        /**
         * 获取微博实时热搜榜
         *
         * @return 微博实时热搜榜
         */
        public WeiboHotSearchResponse getWeiboHotSearch() throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.weiboHotSearch.getPath(), "GET", null, WeiboHotSearchResponse.class);
        }
    
        /**
         * 获取星座运势
         *
         * @param horoscope 星座运势
         * @return 星座运势
         */
        public HoroscopeResponse getHoroscope(HoroscopeParams horoscope) throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.horoscope.getPath(), "POST", horoscope, HoroscopeResponse.class);
        }
    
        /**
         * 公网ip
         *
         * @param publicIp 公网ip
         * @return 公网ip
         */
        public PublicIpResponse getPublicIp(PublicIpParams publicIp) throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.publicIp.getPath(), "POST", publicIp, PublicIpResponse.class);
        }
    
        /**
         * 随机壁纸
         *
         * @return 随机壁纸
         * @throws GigotApiException
         */
        public RandomWallpaperResponse getRandomWallpaper() throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.randomWallpaper.getPath(), "GET", null, RandomWallpaperResponse.class);
        }
    
        /**
         * 土味情话
         * @return 土味情话
         * @throws GigotApiException
         */
        public LoveTalkResponse getLoveTalk() throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.loveTalk.getPath(), "GET", null, LoveTalkResponse.class);
        }
    
        /**
         * 天气
         * @return 天气
         * @throws GigotApiException
         */
        public WeatherResponse getWeather() throws GigotApiException {
            return sendRequest(GATEWAY_URL + UrlToMethodEnum.weather.getPath(), "GET", null, WeatherResponse.class);
        }
    
        // todo 2.添加新的接口调用方法
    
        /**
         * 解析地址和调用接口
         *
         * @param baseRequest
         * @return
         * @throws GigotApiException
         */
        public Object parseAddressAndCallInterface(BaseRequest baseRequest) throws GigotApiException {
            String path = baseRequest.getPath();
            String method = baseRequest.getMethod();
            Map<String, Object> paramsList = baseRequest.getRequestParams();
            HttpServletRequest userRequest = baseRequest.getUserRequest();
            Class<?> clazz = GigotApiClient.class;
            Object result = null;
            try {
                log.info("请求地址:{},请求方法:{},请求参数:{}", path, method, paramsList);
                // 获取名称
                if (path.equals(UrlToMethodEnum.name.getPath())) {
                    return invokeMethod(UrlToMethodEnum.name.getMethod(), paramsList, User.class);
                }
                // 获取毒鸡汤
                if (path.equals(UrlToMethodEnum.poisonousChickenSoup.getPath())) {
                    return invokeMethod(UrlToMethodEnum.poisonousChickenSoup.getMethod());
                }
                // 获取微博热搜榜
                if (path.equals(UrlToMethodEnum.weiboHotSearch.getPath())) {
                    return invokeMethod(UrlToMethodEnum.weiboHotSearch.getMethod());
                }
                // 获取星座运势
                if (path.equals(UrlToMethodEnum.horoscope.getPath())) {
                    return invokeMethod(UrlToMethodEnum.horoscope.getMethod(), paramsList, HoroscopeParams.class);
                }
                // 获取公网ip
                if (path.equals(UrlToMethodEnum.publicIp.getPath())) {
                    // todo 上线时测试是否获取用户的公网ip,目前本机无法获取到X-Real-IP
                    String ipAddress = userRequest.getHeader("X-Real-IP");
                    if (ipAddress == null || ipAddress.isEmpty()) {
                        log.info("未携带X-Real-IP请求头,尝试获取");
                        ipAddress = userRequest.getRemoteAddr();
                    }
                    if (ipAddress == null || ipAddress.isEmpty()) {
                        throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "获取公网ip失败");
                    }
                    log.info("获取到的公网ip:", ipAddress);
                    paramsList.put("ipAddress", ipAddress);
                    return invokeMethod(UrlToMethodEnum.publicIp.getMethod(), paramsList, PublicIpParams.class);
                }
                // 随机壁纸
                if (path.equals(UrlToMethodEnum.randomWallpaper.getPath())) {
                    return invokeMethod(UrlToMethodEnum.randomWallpaper.getMethod());
                }
                // 土味情话
                if (path.equals(UrlToMethodEnum.loveTalk.getPath())) {
                    return invokeMethod(UrlToMethodEnum.loveTalk.getMethod());
                }
                // 天气信息
                if (path.equals(UrlToMethodEnum.weather.getPath())) {
                    return invokeMethod(UrlToMethodEnum.weather.getMethod());
                }
    
                // todo 1.添加新的接口地址判断
            } catch (Exception e) {
                throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "sdk解析地址错误:" + e.getMessage());
            }
            if (ObjUtil.isEmpty(result)) {
                throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "未找到Url对应资源");
            }
            log.info("返回结果:{}", result);
            return result;
        }
    
    
        /**
         * 反射方法(不带参数)
         *
         * @param methodName
         * @return
         * @throws GigotApiException
         */
        private Object invokeMethod(String methodName) throws GigotApiException {
            return this.invokeMethod(methodName, null, null);
        }
    
        /**
         * 反射方法(带参)
         *
         * @param methodName
         * @param params
         * @param paramsType
         * @return
         * @throws GigotApiException
         */
        private Object invokeMethod(String methodName, Map<String, Object> params, Class<?> paramsType) throws GigotApiException {
            try {
                Class<?> clazz = GigotApiClient.class;
                if (params == null) {
                    Method method = clazz.getMethod(methodName);
                    return method.invoke(this);
                } else {
                    log.info("接收到的参数 params:{} paramsType:{}", params, paramsType);
                    Method method = clazz.getMethod(methodName, paramsType);
                    // map转Object
                    Object paramsObject = BeanUtil.mapToBean(params, paramsType, true, CopyOptions.create());
                    log.info("map转Object params:{} paramsType:{}", params, paramsType);
                    return method.invoke(this, paramsObject);
                }
            } catch (Exception e) {
                log.error("调用方法失败",e);
                throw new GigotApiException(ErrorCode.NOT_FOUND_ERROR, "调用方法失败");
            }
        }
    
    
        /**
         * 发送请求
         *
         * @param url          请求地址
         * @param method       请求方式
         * @param params       请求参数
         * @param responseType 响应类型
         * @param <T>          响应类型
         * @return 响应结果
         * @throws GigotApiException 异常
         */
        public <T> T sendRequest(String url, String method, Object params, Class<T> responseType) throws GigotApiException {
            HttpRequest request;
            String jsonBody = JSON.toJSONString(params);
    
            switch (method) {
                case "GET":
                    log.info("params:{}", params);
                    request = HttpRequest.get(url);
                    handleParamsAsQueryParams(request, params);
                    break;
                case "POST":
                    log.info("封装body:{}", jsonBody);
                    request = HttpRequest.post(url);
                    handleParamsAsBody(request, jsonBody);
                    break;
                // Add more cases for other HTTP methods if needed
                default:
                    throw new GigotApiException(ErrorCode.PARAMS_ERROR, "请求方式有误");
            }
    
            // 添加通用请求头
            request.addHeaders(getHeaderMap(jsonBody));
            // 发送请求并获取响应
            HttpResponse response = request.execute();
            // 获取响应体
            String responseBody = response.body();
            log.info("responseBody:{}", responseBody);
            // 解析响应体
            return JSON.parseObject(responseBody, responseType);
        }
    
    
    //    public <T> T sendRequest(String url, String method, Object params, Class<T> responseType) throws GigotApiException {
    //        HttpRequest request;
    //        String jsonBody = JSON.toJSONString(params);
    //
    //        switch (method) {
    //            case "GET":
    //                log.info("params:{}", params);
    //                request = HttpRequest.get(url);
    //                handleParamsAsQueryParams(request, params);
    //                break;
    //            case "POST":
    //                log.info("封装body:{}", jsonBody);
    //                request = HttpRequest.post(url);
    //                handleParamsAsBody(request, jsonBody);
    //                break;
    //            // Add more cases for other HTTP methods if needed
    //            default:
    //                throw new GigotApiException(ErrorCode.PARAMS_ERROR, "请求方式有误");
    //        }
    //
    //        // 添加通用请求头
    //        request.addHeaders(getHeaderMap(jsonBody));
    //        HttpResponse response = request.execute();
    //        String responseBody = response.body();
    //        log.info("responseBody:{}", responseBody);
    //        // Convert the response body to the specified type using JSON deserialization
    //        return JSON.parseObject(responseBody, responseType);
    //    }
    
    
        private void handleParamsAsQueryParams(HttpRequest request, Object bodyAndParams) {
            if (bodyAndParams != null) {
                Map<String, Object> paramsMap = convertObjectToMap(bodyAndParams);
                for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
                    if (entry.getValue() != null) {
                        request.form(entry.getKey(), entry.getValue().toString());
                    }
                }
            }
        }
    
        private void handleParamsAsBody(HttpRequest request, String bodyJson) {
            if (bodyJson != null) {
                // Convert the entire bodyAndParams map to JSON and set it as the request body
                request.header("Content-Type", "application/json; charset=UTF-8")
                        .body(bodyJson);
            }
        }
    
        private Map<String, Object> convertObjectToMap(Object object) {
            // Use reflection to get all fields and their values from the object
            Map<String, Object> map = new HashMap<>();
            Field[] fields = object.getClass().getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                try {
                    map.put(field.getName(), field.get(object));
                } catch (IllegalAccessException e) {
                    // Handle the exception as needed
                }
            }
            return map;
        }
    
        /**
         * 检查配置
         *
         * @param gigotApiClient api客户端
         * @throws GigotApiException 业务异常
         */
        public void checkConfig(GigotApiClient gigotApiClient) throws GigotApiException {
            if (StrUtil.isBlank(gigotApiClient.getSecretId()) && StrUtil.isBlank(gigotApiClient.getSecretKey())) {
                throw new GigotApiException(ErrorCode.NO_AUTH_ERROR, "请先配置密钥AccessKey/SecretKey");
            }
        }
    
        /**
         * 获取请求头Map
         *
         * @param body 请求体
         * @return 请求头Map
         */
        private Map<String, String> getHeaderMap(String body) {
            Map<String, String> headerMap = new HashMap<>();
            headerMap.put("X-SecretId", secretId);
            // 一定不能发送给后端SecretKey, 后端会自动计算签名
            headerMap.put("X-Nonce", RandomUtil.randomNumbers(4));
            headerMap.put("X-Body", body);
            headerMap.put("X-Timestamp", System.currentTimeMillis() + "");
            headerMap.put("X-Sign", SignUtils.getSign(body, secretKey));
            return headerMap;
        }
    
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384

# 接口请求日志记录

  1. 调用时间:记录每次调用API的时间,方便后续分析和定位问题。 ✔
  2. 请求方法:GET、POST、PUT、DELETE等请求方法。 ✔
  3. 请求URL:调用的API接口URL。 ✔
  4. 响应时间:API接口返回响应的时间。 ✔
  5. 响应状态码:API接口返回的HTTP状态码。 ✔
  6. 调用次数:记录每次调用API的次数,可以统计API的调用频率和使用情况。 ✔
  7. 调用成本:记录API的请求和响应流量(通过请求头和响应体实现)✔

创建数据库表:

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80033 (8.0.33)
 Source Host           : localhost:3306
 Source Schema         : gigot_api

 Target Server Type    : MySQL
 Target Server Version : 80033 (8.0.33)
 File Encoding         : 65001

 Date: 23/02/2024 15:32:24
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for interface_log
-- ----------------------------
DROP TABLE IF EXISTS `interface_log`;
CREATE TABLE `interface_log`  (
  `id` bigint NOT NULL COMMENT 'id',
  `interfaceId` bigint NOT NULL COMMENT '接口id',
  `requestTime` datetime NULL DEFAULT NULL COMMENT '请求时间',
  `requestMethod` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求方式',
  `requestUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求地址',
  `requestContentLength` bigint NULL DEFAULT NULL COMMENT '请求长度(上传流量)',
  `responseStatusCode` int NULL DEFAULT NULL COMMENT '响应码',
  `responseContentLength` bigint NULL DEFAULT NULL COMMENT '响应长度(下载流量)',
  `requestDuration` bigint NULL DEFAULT NULL COMMENT '请求处理时间',
  `userId` bigint NULL DEFAULT NULL COMMENT '用户id',
  `clientIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户ip',
  `createTime` datetime NOT NULL COMMENT '创建时间',
  `updateTime` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `isDelete` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否删除(0-未删除,1-已删除)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

添加mybatis三件套参考github (opens new window)

提供Dubbo的Inner

package top.panyuwen.gigotapi.service.impl.inner;

import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import top.panyuwen.gigotapi.common.ErrorCode;
import top.panyuwen.gigotapi.exception.BusinessException;
import top.panyuwen.gigotapi.service.InterfaceLogService;
import top.panyuwen.gigotapicommon.model.entity.InterfaceLog;
import top.panyuwen.gigotapicommon.service.InnerInterfaceLogService;

/**
 * @author PYW
 */
@DubboService
@Slf4j
public class InnerInterfaceLogServiceImpl implements InnerInterfaceLogService {

    @Autowired
    InterfaceLogService interfaceLogService;

    @Override
    public boolean save(InterfaceLog interfaceLog) {
        if(interfaceLog == null){
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "interfaceLog为空");
        }
        interfaceLogService.validInterfaceLog(interfaceLog, true);
        log.info("存储接口调用日志:",interfaceLog);
        return interfaceLogService.save(interfaceLog);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

网关修改:添加请求头大小,响应体大小等,用于记录每次的请求响应长度

package top.panyuwen.gigotapigateway.filter;

import cn.hutool.core.date.StopWatch;
import cn.hutool.core.util.ObjUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import top.panyuwen.gigotapiclientsdk.exception.GigotApiException;
import top.panyuwen.gigotapiclientsdk.utils.SignUtils;
import top.panyuwen.gigotapicommon.model.entity.InterfaceInfo;
import top.panyuwen.gigotapicommon.model.entity.InterfaceLog;
import top.panyuwen.gigotapicommon.model.entity.User;
import top.panyuwen.gigotapicommon.service.InnerInterfaceInfoService;
import top.panyuwen.gigotapicommon.service.InnerInterfaceLogService;
import top.panyuwen.gigotapicommon.service.InnerUserInterfaceInfoService;
import top.panyuwen.gigotapicommon.service.InnerUserService;
import top.panyuwen.gigotapigateway.common.ErrorCode;
import top.panyuwen.gigotapigateway.exception.BusinessException;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;

import static top.panyuwen.gigotapigateway.common.ErrorCode.NOT_FOUND_ERROR;

/**
 * 自定义全局过滤器
 * @author PYW
 */
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    @DubboReference
    private InnerUserService innerUserService;

    @DubboReference
    private InnerUserInterfaceInfoService innerUserInterfaceInfoService;

    @DubboReference
    private InnerInterfaceInfoService innerInterfaceInfoService;

    @DubboReference
    private InnerInterfaceLogService interfaceLogService;

    // 开始时间
    private StopWatch stopWatch = new StopWatch();

    // 结束时间
    private Instant endTime = null;

    // 请求方法
    private String method = null;

    // 请求地址
    private String path = null;

    // 请求url
    private String url;

    // 记录请求流量,获取请求内容长度
    private Long requestContentLength = null;

    // 记录响应流量,获取响应内容长度
    private Long responseContentLength = null;

    // 公网ip
    private String ipAddress = null;

    // userId
    private Long userId = null;

    // interfaceInfoId
    private Long interfaceInfoId = null;

    // 响应状态码
    HttpStatus statusCode = null;


    /**
     * 黑名单
     */
    private List<String> blackList = Arrays.asList("");

    /**
     * 设置执行顺序
     * @return
     */
    @Override
    public int getOrder() {
        // 数字越低越先执行
        return -1;
    }

    /**
     * 过滤器执行逻辑
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 计时
        stopWatch.start();
        // 1.请求日志
        log.info("GateWay网关全局过滤器请求日志信息==========>");
        ServerHttpRequest request = exchange.getRequest();
        // 获取请求时间
        // 记录请求流量,获取请求内容长度
        requestContentLength = request.getHeaders().getContentLength();
        // /api/name/user
        path = request.getPath().value();
        method = request.getMethod().toString();
        url = request.getURI().toString().trim();
        // 获取公网ip
        HttpHeaders headers = request.getHeaders();
        ipAddress = headers.getFirst("X-Real-IP");
        if (ipAddress == null || ipAddress.isEmpty()) {
            log.info("未携带X-Real-IP请求头,尝试获取");
            ipAddress = request.getRemoteAddress().getHostString();
        }
        if (ipAddress == null || ipAddress.isEmpty()) {
            throw new BusinessException(NOT_FOUND_ERROR, "获取公网ip失败");
        }
        log.info("请求唯一标识:"  + request.getId());
        log.info("请求路径:" + path);
        log.info("请求完整url:" + url);
        log.info("请求方式:" + method);
        log.info("QueryParams:" + request.getQueryParams());
        log.info("请求流量(bytes):" + requestContentLength);
        String hostString = request.getLocalAddress().getHostString();
        log.info("请求来源地址:" + hostString);
        log.info("<=======================================");

        // 2.黑白名单
        if(blackList.contains(hostString)){
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        // 3,用户鉴权(判断 ak、sk 是否合法)

        String secretId = headers.getFirst("X-SecretId");
        String nonce = headers.getFirst("X-Nonce");
        String body = headers.getFirst("X-Body");
        try {
            body = new String(body.getBytes("ISO-8859-1"), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String timestamp = headers.getFirst("X-Timestamp");
        String sign = headers.getFirst("X-Sign");

        // Api密钥是否分配给用户校验
        User invokeUser = null;
        try{
            invokeUser = innerUserService.getInvokeUser(secretId);
            if(ObjUtil.isEmpty(invokeUser)){
                throw new BusinessException(NOT_FOUND_ERROR,"请求用户密钥不存在");
            }
            // todo 校验用户金币是否为0
        }catch (Exception e){
            throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"获取Invoke异常");
        }
        if(invokeUser == null){
            throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"未查询到用户信息");
        }

        if(!invokeUser.getSecretId().equals(secretId)){
            throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
        }

        // 随机数不得大于一万
        if(Integer.parseInt(nonce) > 10000){
            throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
        }

        // 防重发,时间和当前时间不得超过五分钟
        if(System.currentTimeMillis() - Long.parseLong(timestamp) > 300000){
            throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
        }

        // 解析sign,查询secretKey是否和header中的一致
        String secretKeyInServer = invokeUser.getSecretKey();
        String signInServer = SignUtils.getSign(body,secretKeyInServer);
        userId = invokeUser.getId();
        // 检查请求中的签名是否为空,或者是否与服务器的签名不一致
        if(sign == null || !sign.equals(signInServer)){
            throw new BusinessException(ErrorCode.FORBIDDEN_ERROR,"API密钥解析失败");
        }
        // 5.请求的模拟接口是否存在?
        if("GET".equals(method)){
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            log.info("GET请求参数:" + queryParams);
        } else if("POST".equals(method)){
            // 获取post请求的参数
            log.info("POST请求参数:");
        }
        // 数据库查询模拟接口是否存在,以及请求方法是否匹配
        try{
            InterfaceInfo interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(url, method);
            interfaceInfoId = interfaceInfo.getId();
            log.info("interfaceInfoId:{}",interfaceInfoId);
            if(ObjUtil.isEmpty(interfaceInfo)){
                throw new BusinessException(NOT_FOUND_ERROR,"请求接口不存在");
            }
        }catch (Exception e){
            log.error("获取接口信息失败", e.getMessage());
        }
        // 6.请求转发,调用模拟接口
        // 7.响应日志

        return handleResponse(exchange, chain, interfaceInfoId, invokeUser.getId());
    }


    /**
     * 处理响应
     *
     * @param exchange
     * @param chain
     * @return
     *
     */
    public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain,Long interfaceId, Long userId) {
        try {
            // 获取原始的响应对象
            ServerHttpResponse originalResponse = exchange.getResponse();
            // 获取数据缓冲工厂
            DataBufferFactory bufferFactory = originalResponse.bufferFactory();
            // 获取响应的状态码
            statusCode = originalResponse.getStatusCode();

            // 判断状态码是否为200 OK
            log.info("响应状态码:{}", statusCode.value());
            if(statusCode == HttpStatus.OK) {
                // 创建一个装饰后的响应对象(开始穿装备,增强能力)
                log.info("成功调用方法,开始响应处理");
                ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {

                    // 重写writeWith方法,用于处理响应体的数据
                    // 这段方法就是只要当我们的模拟接口调用完成之后,等它返回结果,
                    // 就会调用writeWith方法,我们就能根据响应结果做一些自己的处理
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        log.info("响应体是否是Flux类型: {}", (body instanceof Flux));
                        // 判断响应体是否是Flux类型
                        if (body instanceof Flux) {
                            Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                            // 返回一个处理后的响应体
                            // (这里就理解为它在拼接字符串,它把缓冲区的数据取出来,一点一点拼接好)
                            return super.writeWith(fluxBody.collectList().flatMap(list -> {
                                log.info("响应结果:" + list);


                                // 调用成功后的处理逻辑
                                try {
                                    // 调用成功

                                    // 接口调用次数+1
                                    log.info("网关调用接口调用次数+1: {}", interfaceId);
                                    innerUserInterfaceInfoService.invokeCount(interfaceId,userId);

                                } catch (Exception e) {
                                    log.error("调用成功后的处理逻辑异常", e.getMessage());
                                }
                                // 读取响应体的内容并转换为字节数组
                                byte[] content = null;
                                if (list.size() > 1 || list.isEmpty()) {
                                    content = list.stream()
                                            .map(DataBuffer::asByteBuffer)
                                            .reduce((buffer1, buffer2) -> {
                                                ByteBuffer mergedBuffer = ByteBuffer.allocate(buffer1.remaining() + buffer2.remaining());
                                                mergedBuffer.put(buffer1);
                                                mergedBuffer.put(buffer2);
                                                mergedBuffer.flip();
                                                return mergedBuffer;
                                            })
                                            .orElse(ByteBuffer.allocate(0))
                                            .array();
                                } else {
                                    // 处理单个数据块
                                    DataBuffer dataBuffer = list.get(0);
                                    // 读取响应体的内容并转换为字节数组
                                    content = new byte[dataBuffer.readableByteCount()];
                                    dataBuffer.read(content);
                                    DataBufferUtils.release(dataBuffer); // 释放掉内存
                                }
                                // 在这里执行您的后续处理逻辑
                                // 读取响应体的内容并转换为字节数组
                                // 得到响应体
                                responseContentLength = Long.valueOf(content.length);
                                String data = new String(content, StandardCharsets.UTF_8);
                                // 得到响应体大小

                                // 打印日志
                                log.info("响应结果:" + data);

                                // 返回一个包含处理后的响应体的 DataBuffer
                                return Mono.just(bufferFactory.wrap(content))
                                        .doFinally(signalType -> logSave());
                            }));
                        } else {
                            log.error("网关处理响应异常", getStatusCode());
                        }
                        return super.writeWith(body);
                    }
                };
                // 对于200 OK的请求,将装饰后的响应对象传递给下一个过滤器链,并继续处理(设置repsonse对象为装饰过的)
                // 记录成功日志
                // 获取响应长度

                return chain.filter(exchange.mutate().response(decoratedResponse).build());
            }
            // 对于非200 OK的请求,直接返回,进行降级处理
            return chain.filter(exchange)
                    .doFinally(signalType -> logSave());
        }catch (Exception e){
            // 处理异常情况,记录错误日志
            log.error("gateway log exception.\n" + e);
            return chain.filter(exchange);
        }
    }

    private void logSave(){
        log.info("开始存储日志......");
        stopWatch.stop();
        interfaceLogSave();
    }

    private void interfaceLogSave() {

        InterfaceLog interfaceLog = new InterfaceLog();
        interfaceLog.setInterfaceId(interfaceInfoId);
        interfaceLog.setRequestTime(null);
        interfaceLog.setRequestMethod(method);
        interfaceLog.setRequestUrl(url);
        interfaceLog.setRequestContentLength(requestContentLength);
        interfaceLog.setResponseStatusCode(statusCode.value());
        interfaceLog.setResponseContentLength(responseContentLength);
        interfaceLog.setUserId(userId);
        interfaceLog.setClientIp(ipAddress);
        interfaceLog.setRequestDuration(stopWatch.getTotalTimeMillis());
        interfaceLogService.save(interfaceLog);

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371

# 商品信息

  • 创建数据库表

    /*
     Navicat Premium Data Transfer
    
     Source Server         : localhost
     Source Server Type    : MySQL
     Source Server Version : 80033 (8.0.33)
     Source Host           : localhost:3306
     Source Schema         : gigot_api
    
     Target Server Type    : MySQL
     Target Server Version : 80033 (8.0.33)
     File Encoding         : 65001
    
     Date: 04/03/2024 22:41:26
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for product_info
    -- ----------------------------
    DROP TABLE IF EXISTS `product_info`;
    CREATE TABLE `product_info`  (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
      `name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '产品名称',
      `description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '产品描述',
      `userId` bigint NULL DEFAULT NULL COMMENT '创建人',
      `total` bigint NULL DEFAULT NULL COMMENT '金额(分)',
      `addGoldCoin` bigint NOT NULL DEFAULT 0 COMMENT '增加积分个数',
      `productType` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'RECHARGE' COMMENT '产品类型(VIP-会员 RECHARGE-充值,RECHARGEACTIVITY-充值活动 EXPERIENCE-体验)',
      `status` tinyint NOT NULL DEFAULT 0 COMMENT '商品状态(0- 默认下线 1- 上线)',
      `expirationTime` datetime NULL DEFAULT NULL COMMENT '过期时间',
      `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
      `isDelete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1697087470134259714 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '产品信息' ROW_FORMAT = DYNAMIC;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
  • 业务逻辑

    • 这个其实就很简单了,增删改查就可以了,目前商品较少只需要数据库进行维护就好了,不做前端了
  • 代码

    • Controller

      package top.panyuwen.gigotapi.controller;
      
      import cn.hutool.core.collection.CollUtil;
      import cn.hutool.core.util.ObjUtil;
      import com.alibaba.fastjson.JSON;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.BeanUtils;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.web.bind.annotation.*;
      import top.panyuwen.gigotapi.annotation.AuthCheck;
      import top.panyuwen.gigotapi.common.BaseResponse;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.common.ResultUtils;
      import top.panyuwen.gigotapi.common.request.DeleteListRequest;
      import top.panyuwen.gigotapi.common.request.DeleteRequest;
      import top.panyuwen.gigotapi.common.request.IdRequest;
      import top.panyuwen.gigotapi.constant.UserConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.*;
      
      import top.panyuwen.gigotapi.model.dto.productinfo.ProductInfoAddRequest;
      import top.panyuwen.gigotapi.model.dto.productinfo.ProductInfoEditRequest;
      import top.panyuwen.gigotapi.model.dto.productinfo.ProductInfoQueryRequest;
      import top.panyuwen.gigotapi.model.dto.productinfo.ProductInfoUpdateRequest;
      import top.panyuwen.gigotapi.service.ProductInfoService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.redis.CacheClient;
      import top.panyuwen.gigotapiclientsdk.client.GigotApiClient;
      import top.panyuwen.gigotapiclientsdk.exception.GigotApiException;
      import top.panyuwen.gigotapiclientsdk.model.BaseRequest;
      import top.panyuwen.gigotapicommon.model.entity.ProductInfo;
      import top.panyuwen.gigotapicommon.model.entity.User;
      import top.panyuwen.gigotapicommon.model.vo.ProductInfoVO;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.util.List;
      import java.util.concurrent.TimeUnit;
      import java.util.stream.Collectors;
      
      import static top.panyuwen.gigotapi.constant.RedisConstants.*;
      
      /**
       * 接口管理
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @RestController
      @RequestMapping("/productInfo")
      @Slf4j
      public class ProductInfoController {
      
          @Resource
          private ProductInfoService productInfoService;
      
          @Resource
          private UserService userService;
      
          @Autowired
          private GigotApiClient gigotApiClient;
      
          @Autowired
          private CacheClient cacheClient;
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          // region 增删改查
      
          /**
           * 创建
           *
           * @param productInfoAddRequest
           * @param request
           * @return
           */
          @PostMapping("/add")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Long> addProductInfo(@RequestBody ProductInfoAddRequest productInfoAddRequest, HttpServletRequest request) {
              if (productInfoAddRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              ProductInfo productInfo = new ProductInfo();
              BeanUtils.copyProperties(productInfoAddRequest, productInfo);
              productInfoService.validProductInfo(productInfo, true);
              User loginUser = userService.getLoginUser(request);
              productInfo.setUserId(loginUser.getId());
              boolean result = productInfoService.save(productInfo);
              ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
              long newProductInfoId = productInfo.getId();
              // 存入缓存
              cacheClient.set(CACHE_PRODUCTINFO_KEY + newProductInfoId, productInfo, CACHE_PRODUCTINFO_TTL, TimeUnit.MINUTES);
              stringRedisTemplate.opsForZSet().add(CACHE_PRODUCTINFO_ALL_KEY, JSON.toJSONString(productInfo), CACHE_PRODUCTINFO_ALL_TTL);
              return ResultUtils.success(newProductInfoId);
          }
      
          /**
           * 删除
           *
           * @param deleteRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteById")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> deleteByIdProductInfo(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
              if (deleteRequest == null || deleteRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              long id = deleteRequest.getId();
              // 判断是否存在
              ProductInfo oldProductInfo = productInfoService.getById(id);
              ThrowUtils.throwIf(oldProductInfo == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可删除
              if (!oldProductInfo.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean b = productInfoService.removeById(id);
              productInfoService.deleteRedisCache(id);
              return ResultUtils.success(b);
          }
      
          /**
           * 批量删除
           *
           * @param deleteListRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteByIds")
          public BaseResponse<Boolean> deleteByIdsProductInfo(@RequestBody DeleteListRequest deleteListRequest, HttpServletRequest request) {
              if (deleteListRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              List<Long> ids = deleteListRequest.getIds();
              // 判断是否存在
              List<ProductInfo> productInfos = productInfoService.listByIds(ids);
              if (productInfos.size() != ids.size()) {
                  List<Long> idsInDB = productInfos.stream().map(ProductInfo::getId).collect(Collectors.toList());
                  List<Long> differentIds = CollUtil.disjunction(ids, idsInDB).stream().collect(Collectors.toList());
                  log.error("differentIds:{}", differentIds);
                  ThrowUtils.throwIf(differentIds.size() > 0, ErrorCode.NOT_FOUND_ERROR, "未找到删除数据:differentIds:" + differentIds);
              }
              // 校验删除人是本人或者管理员
              List<Long> createUserIds = productInfos.stream().map(ProductInfo::getUserId).distinct().collect(Collectors.toList());
              boolean isNotSelfAndAdmin = (createUserIds.size() > 1 || !createUserIds.get(0).equals(user.getId())) && !userService.isAdmin(request);
              if (isNotSelfAndAdmin) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "只允许管理员或者本人删除");
              }
              boolean b = productInfoService.removeByIds(ids);
              List<String> idKeyList = productInfos.stream().map(item -> CACHE_USER_KEY + item.getId()).collect(Collectors.toList());
              Long deleteCount = stringRedisTemplate.delete(idKeyList);
              stringRedisTemplate.delete(CACHE_PRODUCTINFO_ALL_KEY);
              return ResultUtils.success(true);
          }
      
          /**
           * 更新(仅管理员)
           *
           * @param productInfoUpdateRequest
           * @return
           */
          @PostMapping("/update")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> updateProductInfo(@RequestBody ProductInfoUpdateRequest productInfoUpdateRequest) {
              if (productInfoUpdateRequest == null || productInfoUpdateRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              ProductInfo productInfo = new ProductInfo();
              BeanUtils.copyProperties(productInfoUpdateRequest, productInfo);
              // 参数校验
              productInfoService.validProductInfo(productInfo, false);
              long id = productInfoUpdateRequest.getId();
              // 判断是否存在
              ProductInfo oldProductInfo = productInfoService.getById(id);
              ThrowUtils.throwIf(oldProductInfo == null, ErrorCode.NOT_FOUND_ERROR);
              boolean result = productInfoService.updateById(productInfo);
              productInfoService.deleteRedisCache(id);
              return ResultUtils.success(result);
          }
      
      
          /**
           * 根据 id 获取
           *
           * @param id
           * @return
           */
          @GetMapping("/get/vo")
          public BaseResponse<ProductInfoVO> getProductInfoVOById(long id, HttpServletRequest request) {
              if (id <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              ProductInfo productInfo = productInfoService.getProductInfoById(id);
              if (productInfo == null) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
              }
              return ResultUtils.success(productInfoService.getProductInfoVO(productInfo, request));
          }
      
          /**
           * 分页获取列表(封装类)
           *
           * @param productInfoQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/list/page/vo")
          public BaseResponse<Page<ProductInfoVO>> listProductInfoVOByPage(@RequestBody ProductInfoQueryRequest productInfoQueryRequest,
                                                                               HttpServletRequest request) {
              Page<ProductInfo> productInfoPage = productInfoService.listProductInfoByPage(productInfoQueryRequest);
              return ResultUtils.success(productInfoService.getProductInfoVOPage(productInfoPage));
          }
      
          /**
           * 分页获取当前用户创建的资源列表
           *
           * @param productInfoQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/my/list/page/vo")
          public BaseResponse<Page<ProductInfoVO>> listMyProductInfoVOByPage(@RequestBody ProductInfoQueryRequest productInfoQueryRequest,
                                                                                 HttpServletRequest request) {
              if (productInfoQueryRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User loginUser = userService.getLoginUser(request);
              productInfoQueryRequest.setUserId(loginUser.getId());
              long current = productInfoQueryRequest.getCurrent();
              long size = productInfoQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              Page<ProductInfo> productInfoPage = productInfoService.page(new Page<>(current, size),
                      productInfoService.getQueryWrapper(productInfoQueryRequest));
              return ResultUtils.success(productInfoService.getProductInfoVOPage(productInfoPage));
          }
      
          // endregion
      
      //    /**
      //     * 分页搜索(从 ES 查询,封装类)
      //     *
      //     * @param productInfoQueryRequest
      //     * @param request
      //     * @return
      //     */
      //    @PostMapping("/search/page/vo")
      //    public BaseResponse<Page<ProductInfoVO>> searchProductInfoVOByPage(@RequestBody ProductInfoQueryRequest productInfoQueryRequest,
      //            HttpServletRequest request) {
      //        long size = productInfoQueryRequest.getPageSize();
      //        // 限制爬虫
      //        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
      //        Page<ProductInfo> productInfoPage = productInfoService.searchFromEs(productInfoQueryRequest);
      //        return ResultUtils.success(productInfoService.getProductInfoVOPage(productInfoPage, request));
      //    }
      
          /**
           * 编辑(用户)
           *
           * @param productInfoEditRequest
           * @param request
           * @return
           */
          @PostMapping("/edit")
          public BaseResponse<Boolean> editProductInfo(@RequestBody ProductInfoEditRequest productInfoEditRequest, HttpServletRequest request) {
              if (productInfoEditRequest == null || productInfoEditRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              ProductInfo productInfo = new ProductInfo();
              BeanUtils.copyProperties(productInfoEditRequest, productInfo);
              // 参数校验
              productInfoService.validProductInfo(productInfo, false);
              User loginUser = userService.getLoginUser(request);
              long id = productInfoEditRequest.getId();
              // 判断是否存在
              ProductInfo oldProductInfo = productInfoService.getById(id);
              ThrowUtils.throwIf(oldProductInfo == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可编辑
              if (!oldProductInfo.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean result = productInfoService.updateById(productInfo);
              productInfoService.deleteRedisCache(id);
              return ResultUtils.success(result);
          }
      
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
    • Service

      package top.panyuwen.gigotapi.service.impl;
      
      import cn.hutool.core.bean.BeanUtil;
      import cn.hutool.core.util.ObjUtil;
      import com.alibaba.fastjson.JSON;
      import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
      import com.google.gson.Gson;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.commons.collections4.CollectionUtils;
      import org.apache.commons.lang3.ObjectUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.stereotype.Service;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.constant.CommonConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.mapper.ProductInfoMapper;
      import top.panyuwen.gigotapi.model.dto.productinfo.ProductInfoQueryRequest;
      import top.panyuwen.gigotapi.service.ProductInfoService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.SqlUtils;
      import top.panyuwen.gigotapi.utils.redis.CacheClient;
      import top.panyuwen.gigotapicommon.model.entity.ProductInfo;
      import top.panyuwen.gigotapicommon.model.vo.ProductInfoVO;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.util.Comparator;
      import java.util.List;
      import java.util.Set;
      import java.util.concurrent.TimeUnit;
      import java.util.stream.Collectors;
      
      import static top.panyuwen.gigotapi.constant.RedisConstants.*;
      
      /**
       * @author PYW
       * @description 针对表【interface_info(接口信息)】的数据库操作Service实现
       * @createDate 2023-12-05 17:11:19
       */
      @Service
      @Slf4j
      public class ProductInfoServiceImpl extends ServiceImpl<ProductInfoMapper, ProductInfo>
              implements ProductInfoService {
      
          private final static Gson GSON = new Gson();
      
          @Resource
          private UserService userService;
      
          @Autowired
          private CacheClient cacheClient;
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          @Resource
          private ElasticsearchRestTemplate elasticsearchRestTemplate;
      
          /**
           * 非空判断
           *
           * @param productInfo
           * @param add
           */
          @Override
          public void validProductInfo(ProductInfo productInfo, boolean add) {
              if (productInfo == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              String name = productInfo.getName();
              String description = productInfo.getDescription();
      
              Long addGoldCoin = productInfo.getAddGoldCoin();
      
      
              // 创建时,参数不能为空
              if (add) {
                  ThrowUtils.throwIf(StringUtils.isAnyBlank(
                          name),
                          ErrorCode.PARAMS_ERROR,"名称必填!");
              }
              // 有参数则校验
              if (ObjUtil.isNotEmpty(addGoldCoin) && name.length() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "添加金币数不正确");
              }
              if (StringUtils.isNotBlank(description) && description.length() > 8192) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "描述过长");
              }
          }
      
          /**
           * 获取查询包装类
           *
           * @param productInfoQueryRequest
           * @return
           */
          @Override
          public QueryWrapper<ProductInfo> getQueryWrapper(ProductInfoQueryRequest productInfoQueryRequest) {
              QueryWrapper<ProductInfo> queryWrapper = new QueryWrapper<>();
              if (productInfoQueryRequest == null) {
                  return queryWrapper;
              }
              Long id = productInfoQueryRequest.getId();
              String searchText = productInfoQueryRequest.getSearchText();
              String name = productInfoQueryRequest.getName();
              String description = productInfoQueryRequest.getDescription();
              Long total= productInfoQueryRequest.getTotal();
              Long addGoldCoin = productInfoQueryRequest.getAddGoldCoin();
              Integer status = productInfoQueryRequest.getStatus();
              String sortField = productInfoQueryRequest.getSortField();
              String sortOrder = productInfoQueryRequest.getSortOrder();
              // 拼接查询条件
              if (StringUtils.isNotBlank(searchText)) {
                  queryWrapper.like("name", searchText).or().like("description", searchText);
              }
              queryWrapper.like(StringUtils.isNotBlank(name), "name", name);
              queryWrapper.like(StringUtils.isNotBlank(description), "description", description);
              queryWrapper.eq(ObjectUtils.isNotEmpty(status), "status", status);
              queryWrapper.eq(ObjectUtils.isNotEmpty(total), "total", total);
              queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
              queryWrapper.eq(ObjectUtils.isNotEmpty(addGoldCoin), "addGoldCoin", addGoldCoin);
              queryWrapper.eq("isDelete", false);
              queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                      sortField);
              return queryWrapper;
          }
      
      //    @Override
      //    public Page<ProductInfo> searchFromEs(ProductInfoQueryRequest productInfoQueryRequest) {
      //        Long id = productInfoQueryRequest.getId();
      //        String name = productInfoQueryRequest.getName();
      //        String searchText = productInfoQueryRequest.getSearchText();
      //        String description = productInfoQueryRequest.getDescription();
      //        String url = productInfoQueryRequest.getUrl();
      //        String requestHeader = productInfoQueryRequest.getRequestHeader();
      //        String responseHeader = productInfoQueryRequest.getResponseHeader();
      //        Integer status = productInfoQueryRequest.getStatus();
      //        String method = productInfoQueryRequest.getMethod();
      //        Long userId = productInfoQueryRequest.getUserId();
      //        // es 起始页为 0
      //        long current = productInfoQueryRequest.getCurrent();
      //        long pageSize = productInfoQueryRequest.getPageSize();
      //        String sortField = productInfoQueryRequest.getSortField();
      //        String sortOrder = productInfoQueryRequest.getSortOrder();
      //        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
      //        // 过滤
      //        boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
      //        if (id != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
      //        }
      //        if (notId != null) {
      //            boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
      //        }
      //        if (userId != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
      //        }
      //        // 必须包含所有标签
      //        if (CollectionUtils.isNotEmpty(tagList)) {
      //            for (String tag : tagList) {
      //                boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
      //            }
      //        }
      //        // 包含任何一个标签即可
      //        if (CollectionUtils.isNotEmpty(orTagList)) {
      //            BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
      //            for (String tag : orTagList) {
      //                orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
      //            }
      //            orTagBoolQueryBuilder.minimumShouldMatch(1);
      //            boolQueryBuilder.filter(orTagBoolQueryBuilder);
      //        }
      //        // 按关键词检索
      //        if (StringUtils.isNotBlank(searchText)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按标题检索
      //        if (StringUtils.isNotBlank(title)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按内容检索
      //        if (StringUtils.isNotBlank(content)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 排序
      //        SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
      //        if (StringUtils.isNotBlank(sortField)) {
      //            sortBuilder = SortBuilders.fieldSort(sortField);
      //            sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
      //        }
      //        // 分页
      //        PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
      //        // 构造查询
      //        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
      //                .withPageable(pageRequest).withSorts(sortBuilder).build();
      //        SearchHits<ProductInfoEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, ProductInfoEsDTO.class);
      //        Page<ProductInfo> page = new Page<>();
      //        page.setTotal(searchHits.getTotalHits());
      //        List<ProductInfo> resourceList = new ArrayList<>();
      //        // 查出结果后,从 db 获取最新动态数据(比如点赞数)
      //        if (searchHits.hasSearchHits()) {
      //            List<SearchHit<ProductInfoEsDTO>> searchHitList = searchHits.getSearchHits();
      //            List<Long> productInfoIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
      //                    .collect(Collectors.toList());
      //            List<ProductInfo> productInfoList = baseMapper.selectBatchIds(productInfoIdList);
      //            if (productInfoList != null) {
      //                Map<Long, List<ProductInfo>> idProductInfoMap = productInfoList.stream().collect(Collectors.groupingBy(ProductInfo::getId));
      //                productInfoIdList.forEach(productInfoId -> {
      //                    if (idProductInfoMap.containsKey(productInfoId)) {
      //                        resourceList.add(idProductInfoMap.get(productInfoId).get(0));
      //                    } else {
      //                        // 从 es 清空 db 已物理删除的数据
      //                        String delete = elasticsearchRestTemplate.delete(String.valueOf(productInfoId), ProductInfoEsDTO.class);
      //                        log.info("delete productInfo {}", delete);
      //                    }
      //                });
      //            }
      //        }
      //        page.setRecords(resourceList);
      //        return page;
      //    }
      
          @Override
          public ProductInfoVO getProductInfoVO(ProductInfo productInfo, HttpServletRequest request) {
              // 转为vo对象
              ProductInfoVO productInfoVO = ProductInfoVO.objToVo(productInfo);
              return productInfoVO;
          }
      
          @Override
          public ProductInfo getProductInfoById(Long id) {
              return cacheClient.getWithLogicalExpire(CACHE_PRODUCTINFO_KEY,id,ProductInfo.class,CACHE_PRODUCTINFO_TTL, TimeUnit.MINUTES,this::getById);
          }
      
          @Override
          public Page<ProductInfoVO> getProductInfoVOPage(Page<ProductInfo> productInfoPage) {
              List<ProductInfo> productInfoList = productInfoPage.getRecords();
              Page<ProductInfoVO> productInfoVOPage = new Page<>(productInfoPage.getCurrent(), productInfoPage.getSize(), productInfoPage.getTotal());
              if (CollectionUtils.isEmpty(productInfoList)) {
                  return productInfoVOPage;
              }
              List<ProductInfoVO> productInfoVOList = productInfoList.stream().map(productInfo -> {
                  ProductInfoVO productInfoVO = new ProductInfoVO();
                  BeanUtil.copyProperties(productInfo,productInfoVO);
                  return productInfoVO;
              }).collect(Collectors.toList());
              // 2. 已登录,获取用户点赞、收藏状态
              // 填充信息
              productInfoVOPage.setRecords(productInfoVOList);
              return productInfoVOPage;
          }
      
      
          @Override
          public Page<ProductInfo> listProductInfoByPage(ProductInfoQueryRequest productInfoQueryRequest) {
              long current = productInfoQueryRequest.getCurrent();
              long size = productInfoQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              // 从缓存中获取接口分页信息
              // 计算 offset 和 count
              long offset = (current - 1) * size;
              long count = size;
              // 获取redis中的数据
              Set<String> allProductInfosInRedis = stringRedisTemplate.opsForZSet().reverseRange(CACHE_PRODUCTINFO_ALL_KEY, 0, -1);
              Page<ProductInfo> page = new Page<>(current, size);
              if(ObjUtil.isNotEmpty(allProductInfosInRedis)){
                  // 对元素进行分页处理
                  List<ProductInfo> pagedInterfaces = allProductInfosInRedis.stream()
                          .skip(offset)
                          .limit(count)
                          .map(this::convertJSONToProductInfo)
                          .collect(Collectors.toList());
                  page.setRecords(pagedInterfaces);
                  page.setTotal(allProductInfosInRedis.size());
                  return page;
              }
              // 未命中
              // 查询数据库
      
              List<ProductInfo> allProductInfoList = list();
              page.setRecords(allProductInfoList);
              page.setTotal(allProductInfoList.size());
              //存储数据到redis中
              for (ProductInfo productInfo : page.getRecords()) {
                  // 将 ProductInfo 转为 JSON 字符串
                  String json = convertProductInfoToJSON(productInfo);
      
                  // 添加到 Redis 的 ZSet 中,使用默认的 score(即按照插入顺序)
                  stringRedisTemplate.opsForZSet().add(CACHE_PRODUCTINFO_ALL_KEY, json,productInfo.getAddGoldCoin());
      
              }
              return page;
          }
      
          private String convertProductInfoToJSON(ProductInfo productInfo) {
              return JSON.toJSONString(productInfo);
          }
          private ProductInfo convertJSONToProductInfo(String productInfoJSON) {
              return JSON.parseObject(productInfoJSON, ProductInfo.class);
          }
      
          @Override
          public void deleteRedisCache(Long id){
              stringRedisTemplate.delete(CACHE_PRODUCTINFO_ALL_KEY);
              stringRedisTemplate.delete(CACHE_PRODUCTINFO_KEY + id);
          }
      
      
      
      }
      
      
      
      
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327

# 商品订单

  • 创建数据库表

    /*
     Navicat Premium Data Transfer
    
     Source Server         : localhost
     Source Server Type    : MySQL
     Source Server Version : 80033 (8.0.33)
     Source Host           : localhost:3306
     Source Schema         : gigot_api
    
     Target Server Type    : MySQL
     Target Server Version : 80033 (8.0.33)
     File Encoding         : 65001
    
     Date: 04/03/2024 22:36:52
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for interface_info
    -- ----------------------------
    DROP TABLE IF EXISTS `interface_info`;
    CREATE TABLE `interface_info`  (
      `id` bigint NOT NULL DEFAULT 0 COMMENT 'id',
      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '名称',
      `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
      `url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '接口地址',
      `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求类型',
      `requestExample` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '请求示例',
      `requestParams` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '请求参数',
      `responseParams` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '响应参数',
      `requestHeader` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '请求头',
      `responseHeader` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '响应头',
      `returnFormat` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '返回格式(如JSON等)',
      `totalInvokes` bigint NOT NULL DEFAULT 0 COMMENT '调用总次数',
      `payGoldCoin` bigint NOT NULL COMMENT '消费金币',
      `liked` int NOT NULL DEFAULT 0 COMMENT '点赞总数',
      `status` int NOT NULL DEFAULT 0 COMMENT '接口状态(0-关闭,1-开启)',
      `userId` bigint NOT NULL COMMENT '创建人',
      `createTime` datetime NOT NULL COMMENT '创建时间',
      `updateTime` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `isDelete` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否删除(0-未删除,1-已删除)',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '接口信息' ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
  • 业务逻辑

    • 提供订单增删改查
    • 提供查询订单状态,用于前端支付时定时获取支付状态
  • 代码

    • Controller

      package top.panyuwen.gigotapi.controller;
      
      import cn.hutool.core.collection.CollUtil;
      import cn.hutool.core.util.ObjUtil;
      import cn.hutool.core.util.StrUtil;
      import com.alibaba.fastjson.JSON;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.BeanUtils;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.web.bind.annotation.*;
      import top.panyuwen.gigotapi.annotation.AuthCheck;
      import top.panyuwen.gigotapi.common.BaseResponse;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.common.ResultUtils;
      import top.panyuwen.gigotapi.common.request.DeleteListRequest;
      import top.panyuwen.gigotapi.common.request.DeleteRequest;
      import top.panyuwen.gigotapi.common.request.IdRequest;
      import top.panyuwen.gigotapi.constant.UserConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.model.dto.interfaceinfo.*;
      import top.panyuwen.gigotapi.model.dto.productorder.PayCreateOrderRequest;
      import top.panyuwen.gigotapi.model.dto.productorder.ProductOrderAddRequest;
      import top.panyuwen.gigotapi.model.dto.productorder.ProductOrderQueryRequest;
      import top.panyuwen.gigotapi.service.PayService;
      import top.panyuwen.gigotapicommon.model.enums.ProductOrderStatusEnum;
      import top.panyuwen.gigotapi.service.ProductOrderService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.redis.CacheClient;
      import top.panyuwen.gigotapiclientsdk.client.GigotApiClient;
      import top.panyuwen.gigotapiclientsdk.exception.GigotApiException;
      import top.panyuwen.gigotapiclientsdk.model.BaseRequest;
      import top.panyuwen.gigotapicommon.model.entity.ProductOrder;
      import top.panyuwen.gigotapicommon.model.entity.User;
      import top.panyuwen.gigotapicommon.model.vo.ProductOrderVO;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.util.List;
      import java.util.concurrent.TimeUnit;
      import java.util.stream.Collectors;
      
      import static top.panyuwen.gigotapi.constant.RedisConstants.*;
      
      /**
       * 接口管理
       *
       * @author PYW
       * @from www.panyuwen.top
       */
      @RestController
      @RequestMapping("/productOrder")
      @Slf4j
      public class ProductOrderController {
      
          @Resource
          private ProductOrderService productOrderService;
      
          @Resource
          private UserService userService;
      
          @Autowired
          private GigotApiClient gigotApiClient;
      
          @Autowired
          private CacheClient cacheClient;
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          @Resource
          private List<PayService> payServices;
      
          // region 增删改查
      
          /**
           * 新增订单
           */
          @PostMapping("/pay/create")
          public BaseResponse<ProductOrderVO> payCreateProductOrder(@RequestBody PayCreateOrderRequest payCreateOrderRequest, HttpServletRequest request) {
              User loginUser = userService.getLoginUser(request);
              return ResultUtils.success(productOrderService.payCreateProductOrder(payCreateOrderRequest,loginUser.getId()));
          }
      
          /**
           * 查询订单状态
           * @param productOrderQueryRequest
           * @return
           */
          @PostMapping("/get/status")
          public BaseResponse<Boolean> getOrderStatus(@RequestBody ProductOrderQueryRequest productOrderQueryRequest){
              String orderNo = productOrderQueryRequest.getOrderNo();
              if(ObjUtil.isEmpty(orderNo)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              String cacheStatus = stringRedisTemplate.opsForValue().get(CACHE_PRODUCTORDER_STATUS_KEY + orderNo);
              if(StrUtil.isBlank(cacheStatus)){
                  return ResultUtils.success(false);
              }
              ProductOrder productOrder = productOrderService.getProductOrderByOutTradeNo(orderNo);
              if (ProductOrderStatusEnum.SUCCESS.getValue().equals(productOrder.getStatus())) {
                  return ResultUtils.success(true);
              }
              stringRedisTemplate.opsForValue().set(CACHE_PRODUCTORDER_STATUS_KEY + orderNo, Boolean.FALSE.toString(), 5, TimeUnit.MINUTES);
              return ResultUtils.success(false);
          }
      
          /**
           * 删除
           *
           * @param deleteRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteById")
          @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
          public BaseResponse<Boolean> deleteByIdProductOrder(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
              if (deleteRequest == null || deleteRequest.getId() <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              long id = deleteRequest.getId();
              // 判断是否存在
              ProductOrder oldProductOrder = productOrderService.getById(id);
              ThrowUtils.throwIf(oldProductOrder == null, ErrorCode.NOT_FOUND_ERROR);
              // 仅本人或管理员可删除
              if (!oldProductOrder.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
              }
              boolean b = productOrderService.removeById(id);
              productOrderService.deleteRedisCache(id);
              return ResultUtils.success(b);
          }
      
          /**
           * 批量删除
           *
           * @param deleteListRequest
           * @param request
           * @return
           */
          @PostMapping("/deleteByIds")
          public BaseResponse<Boolean> deleteByIdsProductOrder(@RequestBody DeleteListRequest deleteListRequest, HttpServletRequest request) {
              if (deleteListRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User user = userService.getLoginUser(request);
              List<Long> ids = deleteListRequest.getIds();
              // 判断是否存在
              List<ProductOrder> productOrders = productOrderService.listByIds(ids);
              if (productOrders.size() != ids.size()) {
                  List<Long> idsInDB = productOrders.stream().map(ProductOrder::getId).collect(Collectors.toList());
                  List<Long> differentIds = CollUtil.disjunction(ids, idsInDB).stream().collect(Collectors.toList());
                  log.error("differentIds:{}", differentIds);
                  ThrowUtils.throwIf(differentIds.size() > 0, ErrorCode.NOT_FOUND_ERROR, "未找到删除数据:differentIds:" + differentIds);
              }
              // 校验删除人是本人或者管理员
              List<Long> createUserIds = productOrders.stream().map(ProductOrder::getUserId).distinct().collect(Collectors.toList());
              boolean isNotSelfAndAdmin = (createUserIds.size() > 1 || !createUserIds.get(0).equals(user.getId())) && !userService.isAdmin(request);
              if (isNotSelfAndAdmin) {
                  throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "只允许管理员或者本人删除");
              }
              boolean b = productOrderService.removeByIds(ids);
              List<String> idKeyList = productOrders.stream().map(item -> CACHE_USER_KEY + item.getId()).collect(Collectors.toList());
              Long deleteCount = stringRedisTemplate.delete(idKeyList);
              for (Long id : ids) {
                  stringRedisTemplate.delete(CACHE_PRODUCTORDER_KEY + id);
              }
              stringRedisTemplate.delete(CACHE_PRODUCTORDER_MY_ALL_KEY);
              return ResultUtils.success(true);
          }
      
      
          /**
           * 根据 id 获取
           *
           * @param id
           * @return
           */
          @GetMapping("/get/vo")
          public BaseResponse<ProductOrderVO> getProductOrderVOById(long id, HttpServletRequest request) {
              if (id <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              ProductOrder productOrder = productOrderService.getProductOrderById(id);
              if (productOrder == null) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
              }
              return ResultUtils.success(productOrderService.getProductOrderVO(productOrder));
          }
      
          /**
           * 分页获取列表(封装类)
           *
           * @param productOrderQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/list/page/vo")
          public BaseResponse<Page<ProductOrderVO>> listProductOrderVOByPage(@RequestBody ProductOrderQueryRequest productOrderQueryRequest,
                                                                               HttpServletRequest request) {
              Page<ProductOrder> productOrderPage = productOrderService.listProductOrderByPage(productOrderQueryRequest);
              return ResultUtils.success(productOrderService.getProductOrderVOPage(productOrderPage));
          }
      
          /**
           * 分页获取当前用户创建的资源列表
           *
           * @param productOrderQueryRequest
           * @param request
           * @return
           */
          @PostMapping("/my/list/page/vo")
          public BaseResponse<Page<ProductOrderVO>> listMyProductOrderVOByPage(@RequestBody ProductOrderQueryRequest productOrderQueryRequest,
                                                                                 HttpServletRequest request) {
              if (productOrderQueryRequest == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              User loginUser = userService.getLoginUser(request);
              productOrderQueryRequest.setUserId(loginUser.getId());
              long current = productOrderQueryRequest.getCurrent();
              long size = productOrderQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              log.info("准备分页查询");
              Page<ProductOrder> productOrderPage = productOrderService.page(new Page<>(current, size),
                      productOrderService.getQueryWrapper(productOrderQueryRequest));
              log.info("分页查询结果:productOrderPage:{}",productOrderPage.getRecords());
              return ResultUtils.success(productOrderService.getProductOrderVOPage(productOrderPage));
          }
      
          // endregion
      
      //    /**
      //     * 分页搜索(从 ES 查询,封装类)
      //     *
      //     * @param productOrderQueryRequest
      //     * @param request
      //     * @return
      //     */
      //    @PostMapping("/search/page/vo")
      //    public BaseResponse<Page<ProductOrderVO>> searchProductOrderVOByPage(@RequestBody ProductOrderQueryRequest productOrderQueryRequest,
      //            HttpServletRequest request) {
      //        long size = productOrderQueryRequest.getPageSize();
      //        // 限制爬虫
      //        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
      //        Page<ProductOrder> productOrderPage = productOrderService.searchFromEs(productOrderQueryRequest);
      //        return ResultUtils.success(productOrderService.getProductOrderVOPage(productOrderPage, request));
      //    }
      
      
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
    • Service

      package top.panyuwen.gigotapi.service.impl;
      
      import cn.hutool.core.util.*;
      import com.alibaba.fastjson.JSON;
      import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
      import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
      import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
      import com.google.gson.Gson;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.commons.collections4.CollectionUtils;
      import org.apache.commons.lang3.ObjectUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.stereotype.Service;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.constant.CommonConstant;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.exception.ThrowUtils;
      import top.panyuwen.gigotapi.mapper.ProductOrderMapper;
      import top.panyuwen.gigotapi.model.dto.productorder.PayCreateOrderRequest;
      import top.panyuwen.gigotapi.model.dto.productorder.ProductOrderQueryRequest;
      import top.panyuwen.gigotapi.service.ProductInfoService;
      import top.panyuwen.gigotapi.service.ProductOrderService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.service.PayService;
      import top.panyuwen.gigotapi.utils.MailUtils;
      import top.panyuwen.gigotapi.utils.SqlUtils;
      import top.panyuwen.gigotapi.utils.redis.CacheClient;
      import top.panyuwen.gigotapicommon.model.entity.ProductInfo;
      import top.panyuwen.gigotapicommon.model.entity.ProductOrder;
      import top.panyuwen.gigotapicommon.model.entity.User;
      import top.panyuwen.gigotapicommon.model.enums.PayTypeEnum;
      import top.panyuwen.gigotapicommon.model.enums.ProductOrderStatusEnum;
      import top.panyuwen.gigotapicommon.model.vo.ProductOrderVO;
      
      import javax.annotation.Resource;
      import javax.mail.MessagingException;
      import javax.servlet.http.HttpServletRequest;
      import java.io.IOException;
      import java.io.InputStream;
      import java.math.BigDecimal;
      import java.math.RoundingMode;
      import java.time.LocalDateTime;
      import java.time.ZoneOffset;
      import java.util.List;
      import java.util.Set;
      import java.util.concurrent.TimeUnit;
      import java.util.stream.Collectors;
      
      import static top.panyuwen.gigotapi.constant.RedisConstants.*;
      
      /**
      * @author PYW
      * @description 针对表【product_order(商品订单)】的数据库操作Service实现
      * @createDate 2024-01-10 14:39:44
      */
      @Service
      @Slf4j
      public class ProductOrderServiceImpl extends ServiceImpl<ProductOrderMapper, ProductOrder>
          implements ProductOrderService {
      
          private final static Gson GSON = new Gson();
      
          public static final String PRODUCT_ORDER_PREFIX = "PO-";
      
          @Resource
          private UserService userService;
      
          @Resource
          private CacheClient cacheClient;
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          @Resource
          private ElasticsearchRestTemplate elasticsearchRestTemplate;
      
          @Resource
          private ProductInfoService productInfoService;
      
          @Resource
          private List<PayService> payServices;
      
          @Autowired
          private MailUtils mailUtils;
      
          /**
           * 按付费类型获取产品订单服务
           *
           * @param payType 付款类型
           * @return {@link ProductOrderService}
           */
          @Override
          public PayService getProductOrderServiceByPayType(String payType) {
              return payServices.stream()
                      .filter(s -> {
                          Qualifier qualifierAnnotation = s.getClass().getAnnotation(Qualifier.class);
                          return qualifierAnnotation != null && qualifierAnnotation.value().equals(payType);
                      })
                      .findFirst()
                      .orElseThrow(() -> new BusinessException(ErrorCode.PARAMS_ERROR, "暂无该支付方式"));
          }
      
          @Override
          public String doOrderNotify(String notifyData, HttpServletRequest request) {
              String payType;
              if (notifyData.startsWith("gmt_create=") && notifyData.contains("gmt_create") && notifyData.contains("sign_type") && notifyData.contains("notify_type")) {
                  payType = PayTypeEnum.ALIPAY.getValue();
              } else {
                  payType = PayTypeEnum.WX.getValue();
              }
              // 获取支付类型对应的serviceImpl
              PayService productOrderServiceByPayType = this.getProductOrderServiceByPayType(payType);
      
              return productOrderServiceByPayType.doPaymentNotify(notifyData, request);
          }
      
          @Override
          public boolean updateStatusByOutTradeNo(String outTradeNo, String status) {
              LambdaUpdateWrapper<ProductOrder> updateWrapper = new LambdaUpdateWrapper<ProductOrder>();
              updateWrapper.eq(ProductOrder::getOrderNo, outTradeNo);
              updateWrapper.set(ProductOrder::getStatus, status);
              return this.update(updateWrapper);
          }
      
          @Override
          public void sendPaidEmail(ProductOrder productOrder) {
              // 非空判断
              ThrowUtils.throwIf(ObjectUtils.isEmpty(productOrder), ErrorCode.PARAMS_ERROR, "订单信息为空!");
              String orderNo = productOrder.getOrderNo();
              String orderName = productOrder.getOrderName();
              Long total = productOrder.getTotal();
              if(StrUtil.isBlank(orderNo) || StrUtil.isBlank(orderName) || ObjUtil.isEmpty(total)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "订单信息为空!");
              }
              try {
                  // 获取收件人邮箱
                  User user = userService.getById(productOrder.getUserId());
                  // 校验是否绑定邮箱
                  String email = user.getEmail();
                  if(StrUtil.isBlank(email)){
                      // 未绑定邮箱直接返回
                      return;
                  }
                  // 加载html
                  String emailContent = mailUtils.loadEmailTemplate("static/email/paidEmail.html");
                  // 替换模板变量
                  emailContent = mailUtils.populateTemplate(emailContent, orderNo, orderName, String.valueOf(new BigDecimal(productOrder.getTotal()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)));
      
                  // 发送短信
                  mailUtils.sendMail(email,emailContent);
              } catch (MessagingException e) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "邮件发送失败!");
              } catch (IOException e) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "加载邮件模板失败!");
              }
          }
      
      
          /**
           * 非空判断
           *
           * @param productOrder
           * @param add
           */
          @Override
          public void validProductOrder(ProductOrder productOrder, boolean add) {
              if (productOrder == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR);
              }
              Long userId = productOrder.getUserId();
              Long productId = productOrder.getProductId();
              Long total = productOrder.getTotal();
              String orderName = productOrder.getOrderName();
              String payType = productOrder.getPayType();
              Long addGoldCoin = productOrder.getAddGoldCoin();
      
              // 创建时,参数不能为空
              if (add) {
                  ThrowUtils.throwIf(StringUtils.isAnyBlank(
                          orderName, payType),
                          ErrorCode.PARAMS_ERROR,"订单名称,支付类型不能为空!");
                  if(ObjUtil.isEmpty(userId) || ObjUtil.isEmpty(productId) || ObjUtil.isEmpty(total)){
                      throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID,商品ID不能为空,请检查");
                  }
              }
              // 有参数则校验
              if (ObjUtil.isEmpty(total) && total <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "金额必须为正数");
              }
              if (ObjUtil.isEmpty(addGoldCoin) && total <= 0) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "添加金币必须为正数");
              }
          }
      
          /**
           * 获取查询包装类
           *
           * @param productOrderQueryRequest
           * @return
           */
          @Override
          public QueryWrapper<ProductOrder> getQueryWrapper(ProductOrderQueryRequest productOrderQueryRequest) {
              QueryWrapper<ProductOrder> queryWrapper = new QueryWrapper<>();
              if (productOrderQueryRequest == null) {
                  return queryWrapper;
              }
              Long id = productOrderQueryRequest.getId();
              String searchText = productOrderQueryRequest.getSearchText();
              String orderNo = productOrderQueryRequest.getOrderNo();
              Long userId = productOrderQueryRequest.getUserId();
              Long productId = productOrderQueryRequest.getProductId();
              String orderName = productOrderQueryRequest.getOrderName();
              String status = productOrderQueryRequest.getStatus();
              String payType = productOrderQueryRequest.getPayType();
              String productInfo = productOrderQueryRequest.getProductInfo();
              String sortField = productOrderQueryRequest.getSortField();
              String sortOrder = productOrderQueryRequest.getSortOrder();
              // 拼接查询条件
              if (StringUtils.isNotBlank(searchText)) {
                  queryWrapper.like("orderName", searchText).or().like("content", searchText);
              }
              queryWrapper.like(StringUtils.isNotBlank(orderNo), "orderNo", orderNo);
              queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
              queryWrapper.eq(ObjectUtils.isNotEmpty(productId), "productId", productId);
              queryWrapper.like(StringUtils.isNotBlank(orderName), "orderName", orderName);
              queryWrapper.like(StringUtils.isNotBlank(payType), "payType", payType);
              queryWrapper.like(StringUtils.isNotBlank(productInfo), "productInfo", productInfo);
              queryWrapper.eq(ObjectUtils.isNotEmpty(status), "status", status);
              queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                      sortField);
              return queryWrapper;
          }
      
      //    @Override
      //    public Page<ProductOrder> searchFromEs(ProductOrderQueryRequest productOrderQueryRequest) {
      //        Long id = productOrderQueryRequest.getId();
      //        String name = productOrderQueryRequest.getName();
      //        String searchText = productOrderQueryRequest.getSearchText();
      //        String description = productOrderQueryRequest.getDescription();
      //        String url = productOrderQueryRequest.getUrl();
      //        String requestHeader = productOrderQueryRequest.getRequestHeader();
      //        String responseHeader = productOrderQueryRequest.getResponseHeader();
      //        Integer status = productOrderQueryRequest.getStatus();
      //        String method = productOrderQueryRequest.getMethod();
      //        Long userId = productOrderQueryRequest.getUserId();
      //        // es 起始页为 0
      //        long current = productOrderQueryRequest.getCurrent();
      //        long pageSize = productOrderQueryRequest.getPageSize();
      //        String sortField = productOrderQueryRequest.getSortField();
      //        String sortOrder = productOrderQueryRequest.getSortOrder();
      //        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
      //        // 过滤
      //        boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
      //        if (id != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
      //        }
      //        if (notId != null) {
      //            boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
      //        }
      //        if (userId != null) {
      //            boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
      //        }
      //        // 必须包含所有标签
      //        if (CollectionUtils.isNotEmpty(tagList)) {
      //            for (String tag : tagList) {
      //                boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
      //            }
      //        }
      //        // 包含任何一个标签即可
      //        if (CollectionUtils.isNotEmpty(orTagList)) {
      //            BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
      //            for (String tag : orTagList) {
      //                orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
      //            }
      //            orTagBoolQueryBuilder.minimumShouldMatch(1);
      //            boolQueryBuilder.filter(orTagBoolQueryBuilder);
      //        }
      //        // 按关键词检索
      //        if (StringUtils.isNotBlank(searchText)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按标题检索
      //        if (StringUtils.isNotBlank(title)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 按内容检索
      //        if (StringUtils.isNotBlank(content)) {
      //            boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
      //            boolQueryBuilder.minimumShouldMatch(1);
      //        }
      //        // 排序
      //        SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
      //        if (StringUtils.isNotBlank(sortField)) {
      //            sortBuilder = SortBuilders.fieldSort(sortField);
      //            sortBuilder.order(CommonConstant.SORT_ORDER_ASC.equals(sortOrder) ? SortOrder.ASC : SortOrder.DESC);
      //        }
      //        // 分页
      //        PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
      //        // 构造查询
      //        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
      //                .withPageable(pageRequest).withSorts(sortBuilder).build();
      //        SearchHits<ProductOrderEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, ProductOrderEsDTO.class);
      //        Page<ProductOrder> page = new Page<>();
      //        page.setTotal(searchHits.getTotalHits());
      //        List<ProductOrder> resourceList = new ArrayList<>();
      //        // 查出结果后,从 db 获取最新动态数据(比如点赞数)
      //        if (searchHits.hasSearchHits()) {
      //            List<SearchHit<ProductOrderEsDTO>> searchHitList = searchHits.getSearchHits();
      //            List<Long> productOrderIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
      //                    .collect(Collectors.toList());
      //            List<ProductOrder> productOrderList = baseMapper.selectBatchIds(productOrderIdList);
      //            if (productOrderList != null) {
      //                Map<Long, List<ProductOrder>> idProductOrderMap = productOrderList.stream().collect(Collectors.groupingBy(ProductOrder::getId));
      //                productOrderIdList.forEach(productOrderId -> {
      //                    if (idProductOrderMap.containsKey(productOrderId)) {
      //                        resourceList.add(idProductOrderMap.get(productOrderId).get(0));
      //                    } else {
      //                        // 从 es 清空 db 已物理删除的数据
      //                        String delete = elasticsearchRestTemplate.delete(String.valueOf(productOrderId), ProductOrderEsDTO.class);
      //                        log.info("delete productOrder {}", delete);
      //                    }
      //                });
      //            }
      //        }
      //        page.setRecords(resourceList);
      //        return page;
      //    }
      
          @Override
          public ProductOrderVO getProductOrderVO(ProductOrder productOrder) {
              // 转为vo对象
              ProductOrderVO productOrderVO = new ProductOrderVO();
              productOrderVO.setId(productOrder.getId());
              productOrderVO.setOrderNo(productOrder.getOrderNo());
              productOrderVO.setCodeUrl(productOrder.getCodeUrl());
              productOrderVO.setUserId(productOrder.getUserId());
              productOrderVO.setProductId(productOrder.getProductId());
              productOrderVO.setOrderName(productOrder.getOrderName());
              productOrderVO.setTotal(productOrder.getTotal());
              productOrderVO.setStatus(productOrder.getStatus());
              productOrderVO.setPayType(productOrder.getPayType());
              productOrderVO.setProductInfo(JSON.parseObject(productOrder.getProductInfo(), ProductInfo.class));
              productOrderVO.setFormData(productOrder.getFormData());
              productOrderVO.setAddGoldCoin(productOrder.getAddGoldCoin());
              productOrderVO.setExpirationTime(productOrder.getExpirationTime());
              productOrderVO.setCreateTime(productOrder.getCreateTime());
              productOrderVO.setUpdateTime(productOrder.getUpdateTime());
      
      
              return productOrderVO;
          }
      
          @Override
          public ProductOrder getProductOrderById(Long id) {
              return cacheClient.getWithLogicalExpire(CACHE_PRODUCTORDER_KEY,id,ProductOrder.class,CACHE_PRODUCTORDER_TTL, TimeUnit.MINUTES,this::getById);
          }
      
          @Override
          public Page<ProductOrderVO> getProductOrderVOPage(Page<ProductOrder> productOrderPage) {
              List<ProductOrder> productOrderList = productOrderPage.getRecords();
              Page<ProductOrderVO> productOrderVOPage = new Page<>(productOrderPage.getCurrent(), productOrderPage.getSize(), productOrderPage.getTotal());
              if (CollectionUtils.isEmpty(productOrderList)) {
                  return productOrderVOPage;
              }
              List<ProductOrderVO> productOrderVOList = productOrderList.stream().map(productOrder -> {
                  ProductOrderVO productOrderVO = this.getProductOrderVO(productOrder);
                  return productOrderVO;
              }).collect(Collectors.toList());
              // 2. 已登录,获取用户点赞、收藏状态
              // 填充信息
              productOrderVOPage.setRecords(productOrderVOList);
              return productOrderVOPage;
          }
      
      
          @Override
          public Page<ProductOrder> listProductOrderByPage(ProductOrderQueryRequest productOrderQueryRequest) {
              long current = productOrderQueryRequest.getCurrent();
              long size = productOrderQueryRequest.getPageSize();
              // 限制爬虫
              ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
              // 从缓存中获取接口分页信息
              // 计算 offset 和 count
              long offset = (current - 1) * size;
              long count = size;
              // 获取redis中的数据
              Set<String> allInterfacesInRedis = stringRedisTemplate.opsForZSet().reverseRange(CACHE_PRODUCTORDER_MY_ALL_KEY, 0, -1);
              Page<ProductOrder> page = new Page<>(current, size);
              if(ObjUtil.isNotEmpty(allInterfacesInRedis)){
                  //缓存命中
                  log.info("命中");
                  // 对元素进行分页处理
                  List<ProductOrder> pagedInterfaces = allInterfacesInRedis.stream()
                          .skip(offset)
                          .limit(count)
                          .map(this::convertJSONToProductOrder)
                          .collect(Collectors.toList());
                  page.setRecords(pagedInterfaces);
                  page.setTotal(allInterfacesInRedis.size());
                  return page;
              }
              log.info("未命中");
              // 未命中
              // 查询数据库
      
              List<ProductOrder> allProductOrderList = list();
              page.setRecords(allProductOrderList);
              page.setTotal(allProductOrderList.size());
              //存储数据到redis中
              for (ProductOrder productOrder : page.getRecords()) {
                  // 将 ProductOrder 转为 JSON 字符串
                  String json = convertProductOrderToJSON(productOrder);
      
                  // 添加到 Redis 的 ZSet 中,使用默认的 score(即按照插入顺序)
                  stringRedisTemplate.opsForZSet().add(CACHE_PRODUCTORDER_MY_ALL_KEY, json, LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
              }
              return page;
          }
      
          private String convertProductOrderToJSON(ProductOrder productOrder) {
              return JSON.toJSONString(productOrder);
          }
          private ProductOrder convertJSONToProductOrder(String productOrderJSON) {
              return JSON.parseObject(productOrderJSON, ProductOrder.class);
          }
      
          @Override
          public void deleteRedisCache(Long id){
              stringRedisTemplate.delete(CACHE_PRODUCTORDER_MY_ALL_KEY);
              stringRedisTemplate.delete(CACHE_PRODUCTORDER_KEY + id);
          }
      
          @Override
          public ProductOrderVO payCreateProductOrder(PayCreateOrderRequest payCreateOrderRequest, Long userId) {
              String payType = payCreateOrderRequest.getPayType();
              Long productId = payCreateOrderRequest.getProductId();
              if(StringUtils.isBlank(payType)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "payType不能为空");
              }
              if(ObjUtil.isEmpty(productId)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "productId不能为空");
              }
              ProductInfo productInfo = productInfoService.getById(productId);
              String name = productInfo.getName();
              Long total = productInfo.getTotal();
              Long addGoldCoin = productInfo.getAddGoldCoin();
              // 1.健壮性校验
              if(StringUtils.isBlank(name)){
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "数据库productInfo的name为空,请联系管理员");
              }
              if(ObjUtil.isEmpty(total)){
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "数据库productInfo的total为空,请联系管理员");
              }
              if(ObjUtil.isEmpty(addGoldCoin)){
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "数据库productInfo的addGoldCoin为空,请联系管理员");
              }
      
              // 判断订单是否存在
              LambdaQueryWrapper<ProductOrder> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(ProductOrder::getUserId,userId).eq(ProductOrder::getProductId,productId).eq(ProductOrder::getStatus, "1");
              ProductOrder checkProductOrderInDB = getOne(queryWrapper);
              if(ObjectUtil.isNotEmpty(checkProductOrderInDB)){
                  throw new BusinessException(ErrorCode.OPERATION_ERROR, "订单已存在,请勿重复下单");
              }
              String orderNo = PRODUCT_ORDER_PREFIX + RandomUtil.randomNumbers(20);
              // 调用判断支付类型,得到相应支付类型的payService
              PayService payService = getProductOrderServiceByPayType(payType);
              String QRCode = payService.pay(orderNo, name, total.intValue());
      
              if(StrUtil.isBlank(QRCode)){
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "支付二维码生成失败,请联系管理员");
              }
              // 2.创建订单
              ProductOrder productOrder = new ProductOrder();
              // 生成32位纯数字uuid,作为订单号
              long id = IdUtil.getSnowflakeNextId();
      
              productOrder.setId(IdUtil.getSnowflakeNextId());
              productOrder.setOrderNo(orderNo);
              productOrder.setCodeUrl(QRCode);
              productOrder.setUserId(userId);
              productOrder.setProductId(productId);
              productOrder.setProductInfo(JSON.toJSONString(productInfo));
              productOrder.setOrderName(name);
              productOrder.setTotal(total);
              productOrder.setStatus(ProductOrderStatusEnum.NOTPAY.getValue());
              productOrder.setPayType(payType);
              productOrder.setFormData(null);
              productOrder.setAddGoldCoin(addGoldCoin);
              // 15分钟后订单过期
              productOrder.setExpirationTime(LocalDateTime.now().plusMinutes(15));
              boolean save = this.save(productOrder);
              // 存入缓存订单状态
              stringRedisTemplate.opsForValue().set(CACHE_PRODUCTORDER_STATUS_KEY + orderNo, Boolean.FALSE.toString());
              if(!save){
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "创建订单失败");
              }
              this.getProductOrderVO(productOrder);
      
      
              // 获取vo
              ProductOrderVO productOrderVO = this.getProductOrderVO(productOrder);
              // 3.返回
              return productOrderVO;
          }
      
          /**
           * 通过外部编号获得ProductOrder
           * @param outTradeNo
           * @return
           */
          @Override
          public ProductOrder getProductOrderByOutTradeNo(String outTradeNo) {
              LambdaQueryWrapper<ProductOrder> lambdaQueryWrapper = new LambdaQueryWrapper<>();
              lambdaQueryWrapper.eq(ProductOrder::getOrderNo, outTradeNo);
              ProductOrder productOrder = this.getOne(lambdaQueryWrapper);
              if (productOrder == null) {
                  throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
              }
              return productOrder;
          }
      
      
      }
      
      
      
      
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      403
      404
      405
      406
      407
      408
      409
      410
      411
      412
      413
      414
      415
      416
      417
      418
      419
      420
      421
      422
      423
      424
      425
      426
      427
      428
      429
      430
      431
      432
      433
      434
      435
      436
      437
      438
      439
      440
      441
      442
      443
      444
      445
      446
      447
      448
      449
      450
      451
      452
      453
      454
      455
      456
      457
      458
      459
      460
      461
      462
      463
      464
      465
      466
      467
      468
      469
      470
      471
      472
      473
      474
      475
      476
      477
      478
      479
      480
      481
      482
      483
      484
      485
      486
      487
      488
      489
      490
      491
      492
      493
      494
      495
      496
      497
      498
      499
      500
      501
      502
      503
      504
      505
      506
      507
      508
      509
      510
      511
      512
      513
      514
      515
      516
      517
      518
      519
      520
      521
      522
      523
      524
      525
      526
      527
      528
      529
      530
      531
      532
      533
      534
      535
      536
      537
      538
      539

# 微信Native支付

# 前期准备工作

ps:吐槽一下,作为学习需要申请个体户,跑了市监所,说要在手机上弄,各种查资料之类的,被驳回过四五次,也就是给我对接的公务员有耐心,后面还给我打电话告诉我怎么弄;除此之外还需要在微信公众平台申请小程序的,各种资料啊啥的需要准备很多,如果是个人学习听说可以去咸鱼买,我没买过所以需要找找资料了

微信官方文档:https://pay.weixin.qq.com/docs/merchant/products/native-payment/preparation.html

  1. 引入依赖,这个地方要注意了,我使用的这个版本里面的两个依赖版本过低,需要手动引入一下,应该是腾讯的bug吧
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.12</version>
            <exclusions>
                <exclusion>
                    <groupId>com.squareup.okhttp3</groupId>
                    <artifactId>okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.10.0</version>
        </dependency>

        <dependency>
            <groupId>com.squareup.okio</groupId>
            <artifactId>okio</artifactId>
            <version>1.13.0</version>
        </dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 参考文档里面的申请密钥文件之类的
  2. 配置文件:填写自己的相关信息
    • yml
## 微信相关
wx:
  # 微信支付
  pay:
    # 商户号
    mchId: 
    # 商户Api序列号
    mch-serial-no: 
    # 商户私钥文件
    private-key-path: 
    # APIv3秘钥
    api-v3-key: 
    # 微信小程序/公众号/服务号id
    app-id: 
    # 微信服务器地址
    domain: 
    # 接收支付结果地址
    notify-domain: 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 写一个配置文件导入类
package top.panyuwen.gigotapi.config;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
 * @author PYW
 */
@Slf4j
@Configuration
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "wx.pay")
@Data
public class WxPayConfig {

    /**
     * 商户号
     */
    private String mchId;

    /**
     * 商户Api序列号
     */
    private String mchSerialNo;

    /**
     * 商户私钥文件
     */
    private String privateKeyPath;

    /**
     * APIv3秘钥
     */
    private String apiV3Key;

    /**
     * 微信小程序/公众号/服务号id
     */
    private String appId;

    /**
     * 微信服务器地址
     */
    private String domain;

    /**
     * 接收支付结果地址
     */
    private String notifyDomain;


    public NotificationConfig getNotificationConfig() {
        // 使用自动更新平台证书的RSA配置
        // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
        return new RSAAutoCertificateConfig.Builder()
                .merchantId(mchId)
                .privateKeyFromPath(privateKeyPath)
                .merchantSerialNumber(mchSerialNo)
                .apiV3Key(apiV3Key)
                .build();
    }

    @Override
    public String toString() {
        return "WxPayConfig{" +
                "mchId='" + mchId + '\'' +
                ", mchSerialNo='" + mchSerialNo + '\'' +
                ", privateKeyPath='" + privateKeyPath + '\'' +
                ", apiV3Key='" + apiV3Key + '\'' +
                ", appId='" + appId + '\'' +
                ", domain='" + domain + '\'' +
                ", notifyDomain='" + notifyDomain + '\'' +
                '}';
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

# 代码

  1. 写一个我们自己的微信支付工具类

    package top.panyuwen.gigotapi.utils;
    import com.wechat.pay.java.core.notification.NotificationConfig;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateField;
    import cn.hutool.core.date.DateUtil;
    import com.wechat.pay.java.core.Config;
    import com.wechat.pay.java.core.RSAAutoCertificateConfig;
    import com.wechat.pay.java.core.notification.RequestParam;
    import com.wechat.pay.java.service.payments.nativepay.NativePayService;
    import com.wechat.pay.java.service.payments.nativepay.model.Amount;
    import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
    import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.web.ServerProperties;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;
    import top.panyuwen.gigotapi.common.ErrorCode;
    import top.panyuwen.gigotapi.config.WxPayConfig;
    import top.panyuwen.gigotapi.exception.BusinessException;
    
    import javax.servlet.http.HttpServletRequest;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Date;
    
    /**
     * @Author: PYW
     * @Date: 2024/02/06 08:51:32
     * @Version: 1.0
     * @Description: wx 签名工具类
     */
    @Slf4j
    @Data
    @Component
    public class WxPayUtil {
        /**
         * 商户号
         */
        private String mchId;
    
        /**
         * 商户Api序列号
         */
        private String mchSerialNo;
    
        /**
         * 商户私钥文件
         */
        private String privateKeyPath;
    
        /**
         * APIv3秘钥
         */
        private String apiV3Key;
    
        /**
         * 微信小程序/公众号/服务号id
         */
        private String appId;
    
        /**
         * 微信服务器地址
         */
        private String domain;
    
        /**
         * 接收支付结果地址
         */
        private String notifyDomain;
    
        @Autowired
        public WxPayUtil(WxPayConfig wxPayConfig) {
            this.mchId = wxPayConfig.getMchId();
            this.mchSerialNo = wxPayConfig.getMchSerialNo();
            this.privateKeyPath = wxPayConfig.getPrivateKeyPath();
            this.apiV3Key = wxPayConfig.getApiV3Key();
            this.appId = wxPayConfig.getAppId();
            this.domain = wxPayConfig.getDomain();
            this.notifyDomain = wxPayConfig.getNotifyDomain();
        }
    
        /**
         *
         * @param description 商品标题
         * @param outTradeNo 订单号(由接入系统生成,可作为微信支付的查询唯一标识)
         * @param Amount 支付金额
         * @param expirationTime 过期时间
         * @return 微信支付二维码地址
         */
        public String generateWeChatPayQRCodeUrl(String outTradeNo, String description , Integer Amount , LocalDateTime expirationTime) {
            // 输出所有成员变量的值
            log.info("WxPayUtil:{}", this.toString());
            log.info("WxPayUtilParams: outTradeNo:{} Amount:{} expirationTime:{}", outTradeNo, Amount, expirationTime);
            // 构建service
            Config config =
                    new RSAAutoCertificateConfig.Builder()
                            .merchantId(mchId)
                            .privateKeyFromPath(privateKeyPath)
                            .merchantSerialNumber(mchSerialNo)
                            .apiV3Key(apiV3Key)
                            .build();
            // 构建service
            NativePayService service = new NativePayService.Builder().config(config).build();
            // request.setXxx(val)设置所需参数,具体参数可见Request定义
            PrepayRequest request = new PrepayRequest();
            Amount amount = new Amount();
            amount.setTotal(Amount);
            request.setAmount(amount);
            request.setAppid(appId);
            request.setMchid(mchId);
            request.setNotifyUrl(notifyDomain);
            request.setDescription(description);
            request.setOutTradeNo(outTradeNo);
            // 设置订单的过期时间
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
            // 格式化为 RFC3339 格式的字符串
            String format = expirationTime.format(formatter);
            request.setTimeExpire(format);
            // 调用下单方法,得到应答
            PrepayResponse response = null;
            try{
                response = service.prepay(request);
            }catch (Exception e){
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            }
    
            log.info("获取到的二维码地址:{}", response.getCodeUrl());
            // 返回二维码
            return response.getCodeUrl();
        }
    
        /**
         * 获取回调请求头:签名相关
         *
         * @param request HttpServletRequest
         * @return SignatureHeader
         */
        public static RequestParam getRequestParam(String requestBody,HttpServletRequest request) {
            // 获取通知签名
            String wechatSignature = request.getHeader("Wechatpay-Signature");
            String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
            String wechatPaySerial = request.getHeader("Wechatpay-Serial");
            String wechatTimestamp = request.getHeader("Wechatpay-Timestamp");
    
            // 获取请求体
    
    
            // 构造 RequestParam
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(wechatPaySerial)
                    .nonce(wechatpayNonce)
                    .signature(wechatSignature)
                    .timestamp(wechatTimestamp)
                    //获取请求体
                    .body(requestBody)
                    .build();
            return requestParam;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
  2. 新增订单的时候发起支付

    
        @Override
        public ProductOrderVO payCreateProductOrder(PayCreateOrderRequest payCreateOrderRequest, Long userId) {
            String payType = payCreateOrderRequest.getPayType();
            Long productId = payCreateOrderRequest.getProductId();
            if(StringUtils.isBlank(payType)){
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "payType不能为空");
            }
            if(ObjUtil.isEmpty(productId)){
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "productId不能为空");
            }
            ProductInfo productInfo = productInfoService.getById(productId);
            String name = productInfo.getName();
            Long total = productInfo.getTotal();
            Long addGoldCoin = productInfo.getAddGoldCoin();
            // 1.健壮性校验
            if(StringUtils.isBlank(name)){
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "数据库productInfo的name为空,请联系管理员");
            }
            if(ObjUtil.isEmpty(total)){
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "数据库productInfo的total为空,请联系管理员");
            }
            if(ObjUtil.isEmpty(addGoldCoin)){
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "数据库productInfo的addGoldCoin为空,请联系管理员");
            }
    
            // 判断订单是否存在
            LambdaQueryWrapper<ProductOrder> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(ProductOrder::getUserId,userId).eq(ProductOrder::getProductId,productId).eq(ProductOrder::getStatus, "1");
            ProductOrder checkProductOrderInDB = getOne(queryWrapper);
            if(ObjectUtil.isNotEmpty(checkProductOrderInDB)){
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "订单已存在,请勿重复下单");
            }
            String orderNo = PRODUCT_ORDER_PREFIX + RandomUtil.randomNumbers(20);
            // 调用判断支付类型,得到相应支付类型的payService
            PayService payService = getProductOrderServiceByPayType(payType);
            String QRCode = payService.pay(orderNo, name, total.intValue());
    
            if(StrUtil.isBlank(QRCode)){
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "支付二维码生成失败,请联系管理员");
            }
            // 2.创建订单
            ProductOrder productOrder = new ProductOrder();
            // 生成32位纯数字uuid,作为订单号
            long id = IdUtil.getSnowflakeNextId();
    
            productOrder.setId(IdUtil.getSnowflakeNextId());
            productOrder.setOrderNo(orderNo);
            productOrder.setCodeUrl(QRCode);
            productOrder.setUserId(userId);
            productOrder.setProductId(productId);
            productOrder.setProductInfo(JSON.toJSONString(productInfo));
            productOrder.setOrderName(name);
            productOrder.setTotal(total);
            productOrder.setStatus(ProductOrderStatusEnum.NOTPAY.getValue());
            productOrder.setPayType(payType);
            productOrder.setFormData(null);
            productOrder.setAddGoldCoin(addGoldCoin);
            // 15分钟后订单过期
            productOrder.setExpirationTime(LocalDateTime.now().plusMinutes(15));
            boolean save = this.save(productOrder);
            // 存入缓存订单状态
            stringRedisTemplate.opsForValue().set(CACHE_PRODUCTORDER_STATUS_KEY + orderNo, Boolean.FALSE.toString());
            if(!save){
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "创建订单失败");
            }
            this.getProductOrderVO(productOrder);
    
    
            // 获取vo
            ProductOrderVO productOrderVO = this.getProductOrderVO(productOrder);
            // 3.返回
            return productOrderVO;
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
  3. 微信支付接口:因为后面可能有很多支付,写一个接口统一标准

    package top.panyuwen.gigotapi.service;
    
    import org.springframework.stereotype.Service;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * @author PYW
     */
    public interface PayService {
    
        Integer EXPIRATION_TIME = 15;
    
        /**
         * 微信支付
         * @param outTradeNo
         * @param description
         * @param Amount
         * @return
         */
        String pay(String outTradeNo, String description, Integer Amount);
    
    
        /**
         * 支付回调修改订单状态
         * @param notifyData
         * @param request
         * @return
         */
        String doPaymentNotify(String notifyData, HttpServletRequest request);
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
  4. 微信支付实现类

    package top.panyuwen.gigotapi.service.impl;
    
    import cn.hutool.core.util.StrUtil;
    import com.wechat.pay.java.core.notification.NotificationParser;
    import com.wechat.pay.java.core.notification.RequestParam;
    import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import top.panyuwen.gigotapi.common.ErrorCode;
    import top.panyuwen.gigotapi.config.WxPayConfig;
    import top.panyuwen.gigotapi.exception.BusinessException;
    import top.panyuwen.gigotapi.service.PayService;
    import top.panyuwen.gigotapi.service.PaymentTransactionLogService;
    import top.panyuwen.gigotapi.service.ProductOrderService;
    import top.panyuwen.gigotapi.service.UserService;
    import top.panyuwen.gigotapi.utils.WxPayUtil;
    import top.panyuwen.gigotapicommon.model.entity.ProductOrder;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import java.time.LocalDateTime;
    
    import static top.panyuwen.gigotapi.constant.RedisConstants.CACHE_PRODUCTORDER_STATUS_KEY;
    
    /**
     * @author PYW
     */
    @Service
    @Slf4j
    @Qualifier("WX")
    public class WxPayServiceImpl implements PayService {
    
        /**
         * 订单过期时长
         */
        private static final Integer EXPIRATION_TIME = 15;
    
        private WxPayConfig wxConfig;
    
        @Autowired
        private WxPayUtil wxPayUtil;
    
        @Autowired
        private PaymentTransactionLogService paymentTransactionLogService;
    
        @Autowired
        @Lazy
        private ProductOrderService productOrderService;
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        private UserService userService;
    
        @Autowired
        public WxPayServiceImpl(WxPayConfig wxPayConfig){
            this.wxConfig = wxPayConfig;
        }
    
    
    
        /**
         * 微信支付
         * @param outTradeNo
         * @param description
         * @param Amount
         * @return
         */
        @Override
        public String pay(String outTradeNo, String description, Integer Amount) {
            return wxPayUtil.generateWeChatPayQRCodeUrl(outTradeNo, description, Amount, LocalDateTime.now().plusMinutes(EXPIRATION_TIME));
        }
    
        @Transactional
        @Override
        public String doPaymentNotify(String notifyData, HttpServletRequest request) {
            log.info("【微信支付回调通知处理】:{}", notifyData);
            // 初始化 NotificationParser
            // 以支付通知回调为例,验签、解密并转换成 Transaction
            if(StrUtil.isBlank(notifyData)){
                throw new BusinessException(ErrorCode.PARAMS_ERROR,"微信支付回调notifyData返回为空");
            }
            RequestParam requestParam = WxPayUtil.getRequestParam(notifyData, request);
            NotificationParser parser = new NotificationParser(wxConfig.getNotificationConfig());
            Transaction transaction = parser.parse(requestParam, Transaction.class);
            log.info("【微信支付回调通知解密结果】:{}", transaction);
            String outTradeNo = transaction.getOutTradeNo();
            Transaction.TradeStateEnum tradeState = transaction.getTradeState();
    
            if(Transaction.TradeStateEnum.SUCCESS.equals(tradeState)){
                // 获取订单信息
                ProductOrder productOrder = productOrderService.getProductOrderByOutTradeNo(outTradeNo);
                // 处理重复通知
                if (Transaction.TradeStateEnum.SUCCESS.equals(productOrder.getStatus())) {
                    // 删除缓存支付状态
                    stringRedisTemplate.delete(CACHE_PRODUCTORDER_STATUS_KEY + outTradeNo);
                    // 返回微信成功通知,让他不要再发了
                    return Transaction.TradeStateEnum.SUCCESS.toString();
                }
                // 存储返回结果记录日志到数据库
                boolean createpaymentTransactionLogFlag = paymentTransactionLogService.create(transaction);
                if(!createpaymentTransactionLogFlag){
                    throw new BusinessException(ErrorCode.OPERATION_ERROR,"微信支付回调通知日志存储失败");
                }
                // 修改缓存订单状态
                boolean updateStatusFlag = productOrderService.updateStatusByOutTradeNo(outTradeNo, Transaction.TradeStateEnum.SUCCESS.toString());
                // 修改用户金币
                boolean addGoldCoinFlag = userService.adjustmentGoldCoin(productOrder.getUserId(), productOrder.getAddGoldCoin());
    
                // 判断是否成功
                if(updateStatusFlag && addGoldCoinFlag){
                    // 支付成功发送邮件
                    productOrderService.sendPaidEmail(productOrder);
                    // 更新缓存中的状态
                    stringRedisTemplate.opsForValue().set(CACHE_PRODUCTORDER_STATUS_KEY + outTradeNo,"true");
                    // 更新用户缓存
                    userService.updateUserCache(productOrder.getUserId());
                    // 返回成功
                    return Transaction.TradeStateEnum.SUCCESS.toString();
                }
            }
            if (Transaction.TradeStateEnum.PAYERROR.equals(tradeState)) {
                log.error("【微信支付失败】" + transaction);
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信支付失败");
            }
            if (Transaction.TradeStateEnum.USERPAYING.equals(tradeState)) {
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信支付中....");
            }
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "支付失败");
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
  5. 微信支付成功后我们要接收微信的支付成功通知,如果我们给微信响应失败,微信会在以下时间再次发起通知15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m

    • Controller

      package top.panyuwen.gigotapi.controller;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.web.bind.annotation.PostMapping;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      import top.panyuwen.gigotapi.common.BaseResponse;
      import top.panyuwen.gigotapi.common.ResultUtils;
      import top.panyuwen.gigotapi.service.ProductOrderService;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      
      /**
       * 支付
       * @author PYW
       */
      @RestController
      @RequestMapping("/pay")
      @Slf4j
      public class PayController {
      
          @Resource
          private ProductOrderService productOrderService;
      
          /**
           * 解析订单通知结果
           * 通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m
           *
           * @param notifyData 通知数据
           * @param request    请求
           * @return {@link String}
           */
          @PostMapping("/order/notify")
          public BaseResponse<String> parseOrderNotifyResult(@RequestBody String notifyData, HttpServletRequest request) {
              return ResultUtils.success(productOrderService.doOrderNotify(notifyData, request));
          }
      
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
    • 微信支付实现类

      package top.panyuwen.gigotapi.service.impl;
      
      import cn.hutool.core.util.StrUtil;
      import com.wechat.pay.java.core.notification.NotificationParser;
      import com.wechat.pay.java.core.notification.RequestParam;
      import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.context.annotation.Lazy;
      import org.springframework.data.redis.core.StringRedisTemplate;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Transactional;
      import top.panyuwen.gigotapi.common.ErrorCode;
      import top.panyuwen.gigotapi.config.WxPayConfig;
      import top.panyuwen.gigotapi.exception.BusinessException;
      import top.panyuwen.gigotapi.service.PayService;
      import top.panyuwen.gigotapi.service.PaymentTransactionLogService;
      import top.panyuwen.gigotapi.service.ProductOrderService;
      import top.panyuwen.gigotapi.service.UserService;
      import top.panyuwen.gigotapi.utils.WxPayUtil;
      import top.panyuwen.gigotapicommon.model.entity.ProductOrder;
      
      import javax.annotation.Resource;
      import javax.servlet.http.HttpServletRequest;
      import java.time.LocalDateTime;
      
      import static top.panyuwen.gigotapi.constant.RedisConstants.CACHE_PRODUCTORDER_STATUS_KEY;
      
      /**
       * @author PYW
       */
      @Service
      @Slf4j
      @Qualifier("WX")
      public class WxPayServiceImpl implements PayService {
      
          /**
           * 订单过期时长
           */
          private static final Integer EXPIRATION_TIME = 15;
      
          private WxPayConfig wxConfig;
      
          @Autowired
          private WxPayUtil wxPayUtil;
      
          @Autowired
          private PaymentTransactionLogService paymentTransactionLogService;
      
          @Autowired
          @Lazy
          private ProductOrderService productOrderService;
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          @Autowired
          private UserService userService;
      
          @Autowired
          public WxPayServiceImpl(WxPayConfig wxPayConfig){
              this.wxConfig = wxPayConfig;
          }
      
      
      
          /**
           * 微信支付
           * @param outTradeNo
           * @param description
           * @param Amount
           * @return
           */
          @Override
          public String pay(String outTradeNo, String description, Integer Amount) {
              return wxPayUtil.generateWeChatPayQRCodeUrl(outTradeNo, description, Amount, LocalDateTime.now().plusMinutes(EXPIRATION_TIME));
          }
      
          @Transactional
          @Override
          public String doPaymentNotify(String notifyData, HttpServletRequest request) {
              log.info("【微信支付回调通知处理】:{}", notifyData);
              // 初始化 NotificationParser
              // 以支付通知回调为例,验签、解密并转换成 Transaction
              if(StrUtil.isBlank(notifyData)){
                  throw new BusinessException(ErrorCode.PARAMS_ERROR,"微信支付回调notifyData返回为空");
              }
              RequestParam requestParam = WxPayUtil.getRequestParam(notifyData, request);
              NotificationParser parser = new NotificationParser(wxConfig.getNotificationConfig());
              Transaction transaction = parser.parse(requestParam, Transaction.class);
              log.info("【微信支付回调通知解密结果】:{}", transaction);
              String outTradeNo = transaction.getOutTradeNo();
              Transaction.TradeStateEnum tradeState = transaction.getTradeState();
      
              if(Transaction.TradeStateEnum.SUCCESS.equals(tradeState)){
                  // 获取订单信息
                  ProductOrder productOrder = productOrderService.getProductOrderByOutTradeNo(outTradeNo);
                  // 处理重复通知
                  if (Transaction.TradeStateEnum.SUCCESS.equals(productOrder.getStatus())) {
                      // 删除缓存支付状态
                      stringRedisTemplate.delete(CACHE_PRODUCTORDER_STATUS_KEY + outTradeNo);
                      // 返回微信成功通知,让他不要再发了
                      return Transaction.TradeStateEnum.SUCCESS.toString();
                  }
                  // 存储返回结果记录日志到数据库
                  boolean createpaymentTransactionLogFlag = paymentTransactionLogService.create(transaction);
                  if(!createpaymentTransactionLogFlag){
                      throw new BusinessException(ErrorCode.OPERATION_ERROR,"微信支付回调通知日志存储失败");
                  }
                  // 修改缓存订单状态
                  boolean updateStatusFlag = productOrderService.updateStatusByOutTradeNo(outTradeNo, Transaction.TradeStateEnum.SUCCESS.toString());
                  // 修改用户金币
                  boolean addGoldCoinFlag = userService.adjustmentGoldCoin(productOrder.getUserId(), productOrder.getAddGoldCoin());
      
                  // 判断是否成功
                  if(updateStatusFlag && addGoldCoinFlag){
                      // 支付成功发送邮件
                      productOrderService.sendPaidEmail(productOrder);
                      // 更新缓存中的状态
                      stringRedisTemplate.opsForValue().set(CACHE_PRODUCTORDER_STATUS_KEY + outTradeNo,"true");
                      // 更新用户缓存
                      userService.updateUserCache(productOrder.getUserId());
                      // 返回成功
                      return Transaction.TradeStateEnum.SUCCESS.toString();
                  }
              }
              if (Transaction.TradeStateEnum.PAYERROR.equals(tradeState)) {
                  log.error("【微信支付失败】" + transaction);
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信支付失败");
              }
              if (Transaction.TradeStateEnum.USERPAYING.equals(tradeState)) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信支付中....");
              }
              throw new BusinessException(ErrorCode.OPERATION_ERROR, "支付失败");
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138

# 网关异常处理改造

因为我们发送的都是http请求到网关上,网关返回的响应体是一个html的代码,因此我们需要进行处理获取其中的HTTPStatus和ErrorMsg

package top.panyuwen.gigotapigateway.exception;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
import top.panyuwen.gigotapigateway.common.BaseResponse;
import top.panyuwen.gigotapigateway.common.ResultUtils;

import java.nio.charset.StandardCharsets;

/**
 * @Author: PYW
 * @Date : 2020/12/12
 * @Version: 1.0
 * @Description: 错误web异常处理程序
 */
@Configuration
@Slf4j
@Order(-1)
public class GlobalExceptionHandler implements WebExceptionHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders headers = response.getHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        if (response.isCommitted()) {
            return Mono.error(ex);
        }
        DataBufferFactory bufferFactory = response.bufferFactory();
        response.setStatusCode(HttpStatus.FORBIDDEN);
        BaseResponse<String> error = ResultUtils.error(HttpStatus.FORBIDDEN.value(), ex.getMessage());
        log.error("【网关异常】:{}", error);
        try {
            byte[] errorBytes = objectMapper.writeValueAsBytes(error);
            DataBuffer dataBuffer = bufferFactory.wrap(errorBytes);
            return response.writeWith(Mono.just(dataBuffer));
        } catch (JsonProcessingException e) {
            log.error("JSON序列化异常:{}", e.getMessage());
            return Mono.error(e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 商品订单状态优化

  • 目前我们规划的流程顺序是,创建订单→未支付→已支付,并且在未支付时可以取消订单,如果超过15分钟则自动取消订单

  • 代码

    • 修改订单状态

      
          @Override
          public boolean updateStatusById(ProductOrderUpdateStatusRequest productOrderUpdateStatusRequest) {
              Long id = productOrderUpdateStatusRequest.getId();
              String status = productOrderUpdateStatusRequest.getStatus();
              // 非空判断
              ThrowUtils.throwIf(ObjectUtils.isEmpty(id), ErrorCode.PARAMS_ERROR, "订单信息为空!");
              ThrowUtils.throwIf(StringUtils.isBlank(status), ErrorCode.PARAMS_ERROR,"订单状态不能为空!");
      
              // 查询订单是否存在
              ProductOrder productOrderInDB = this.getById(id);
              if (productOrderInDB == null) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "订单信息不存在!");
              }
      
              // 更新订单
              ProductOrder productOrder = BeanUtil.copyProperties(productOrderUpdateStatusRequest, ProductOrder.class);
      
              // 重构缓存
              this.deleteRedisCache(id);
              return this.updateById(productOrder);
          }
      
          @Override
          public boolean noPayOrderByDurationHandler(PayTypeEnum payTypeEnum) {
              String payType = payTypeEnum.getValue();
              Instant instant = Instant.now().minus(Duration.ofMinutes(EXPIRATION_TIME));
              LambdaQueryWrapper<ProductOrder> productOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
              productOrderLambdaQueryWrapper.eq(ProductOrder::getStatus, ProductOrderStatusEnum.NOTPAY.getValue())
                      .eq(StrUtil.isNotBlank(payType), ProductOrder::getPayType, payType);
              // 现在时间是否超过过期时间
              productOrderLambdaQueryWrapper.and(i -> i.le(ProductOrder::getExpirationTime, instant));
              List<ProductOrder> expirationProductOrders = this.list(productOrderLambdaQueryWrapper);
              // 更新状态
              boolean updateFlag = false;
              for (ProductOrder expirationProductOrder : expirationProductOrders) {
                  LambdaUpdateWrapper<ProductOrder> updateWrapper = new LambdaUpdateWrapper<>();
                  updateWrapper.set(ProductOrder::getStatus, ProductOrderStatusEnum.CLOSED.getValue())
                          .eq(ProductOrder::getId, expirationProductOrder.getId());
                  updateFlag = this.update(updateWrapper);
              }
              return updateFlag;
          }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
    • 定时刷新超时订单

      package top.panyuwen.gigotapibackend.job;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.scheduling.annotation.Scheduled;
      import org.springframework.stereotype.Component;
      import top.panyuwen.gigotapibackend.service.ProductOrderService;
      import top.panyuwen.gigotapibackend.utils.RedissonLockUtil;
      import top.panyuwen.gigotapicommon.model.entity.ProductOrder;
      import top.panyuwen.gigotapicommon.model.enums.PayTypeEnum;
      
      import javax.annotation.Resource;
      import java.util.List;
      
      @Slf4j
      @Component
      public class PayJob {
      
          @Resource
          private ProductOrderService productOrderService;
          @Resource
          private RedissonLockUtil redissonLockUtil;
      
          /**
           * 微信订单确认
           * 每25s查询一次未支付过期的订单
           */
          @Scheduled(cron = "0/25 * * * * ?")
          public void wxOrderConfirm() {
              redissonLockUtil.redissonDistributedLocks("wxOrderConfirm", () -> {
                  productOrderService.noPayOrderByDurationHandler(PayTypeEnum.WX);
              });
          }
      }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34

# 登录功能增强

# 小程序扫码登录

# 理论基础:Oauth2.0

image-20240305145005803

# 业务逻辑

因为是微信小程序,我们需要下载微信开发者工具进行前端开发,后端继续使用我们的backend就好了

  • 业务流程图

    小程序登录系统时序图

  • 代码

    • Controller

          /**
           * 登录(微信小程序,免注册)
           */
          @PostMapping("/login/xcx")
          public BaseResponse<LoginUserVO> userLoginByXcx(@RequestBody UserLoginByXcxRequest userLoginByXcxRequest) {
              String code = userLoginByXcxRequest.getCode();
              log.info("接收到小程序的登录userLoginByXcxRequest:{}", userLoginByXcxRequest);
              LoginUserVO loginUserVO = userService.userLoginByXcx(userLoginByXcxRequest);
              return ResultUtils.success(null);
          }
      
          @PostMapping("/login/xcx/createQR")
          public BaseResponse<LoginXcxQrVO> userLoginByXcxCreateQR(){
              return ResultUtils.success(userService.userLoginByXcxCreateQR());
          }
      
          /**
           * 获取当前登录用户(小程序扫码登录校验)
           *
           * @return
           */
          @PostMapping("/get/login/xcx/check")
          public BaseResponse<LoginUserVO> getLoginUserXcxCheck(@RequestBody UserLoginXcxCheckRequest userLoginXcxCheckRequest) {
              String scene = userLoginXcxCheckRequest.getScene();
              return ResultUtils.success(userService.getLoginUserXcxCheck(scene));
          }
      
          /**
           * 用户登录(微信开放平台)
           */
          @GetMapping("/login/wx_open")
          public BaseResponse<LoginUserVO> userLoginByWxOpen(HttpServletRequest request, HttpServletResponse response,
                                                             @RequestParam("code") String code) {
              WxOAuth2AccessToken accessToken;
              try {
                  WxMpService wxService = wxOpenConfig.getWxMpService();
                  accessToken = wxService.getOAuth2Service().getAccessToken(code);
                  WxOAuth2UserInfo userInfo = wxService.getOAuth2Service().getUserInfo(accessToken, code);
                  String unionId = userInfo.getUnionId();
                  String mpOpenId = userInfo.getOpenid();
                  if (StringUtils.isAnyBlank(unionId, mpOpenId)) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
                  }
                  return ResultUtils.success(userService.userLoginByMpOpen(userInfo, request));
              } catch (Exception e) {
                  log.error("userLoginByWxOpen error", e);
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,系统错误");
              }
          }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
    • Service

          @Override
          public LoginUserVO userLoginByXcx(UserLoginByXcxRequest userLoginByXcxRequest) {
              String code = userLoginByXcxRequest.getCode();
              String scene = userLoginByXcxRequest.getScene();
              // 非空判断
              if (StrUtil.isBlank(code) || StrUtil.isBlank(scene)) {
                  throw new BusinessException(ErrorCode.PARAMS_ERROR, "微信小程序传输的code和scene不能为空");
              }
      
              // 获取配置信息
              String appId = wxMpConfig.getAppId();
              String secret = wxMpConfig.getSecret();
              // 向微信服务端发起请求
              OkHttpClient client = new OkHttpClient();
      
              // 定义请求对象
              Request request = new Request.Builder()
                      .url("https://api.weixin.qq.com/sns/jscode2session?" +
                              "appid=" + appId +
                              "&secret=" + secret +
                              "&js_code=" + code +
                              "&grant_type=authorization_code") // 替换为要请求的网址
                      .build();
      
              Response response = null;
              try {
                  response = client.newCall(request).execute();
              } catch (IOException e) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "请求微信OpenId失败,请稍后再试");
              }
              String body = null;
              try {
                  body = response.body().string();
              } catch (IOException e) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "请求微信服务器响应体转换字符串失败");
              }
      
              log.info("微信服务器登录返回响应体:{}", body);
              WxMpLoginResponse wxMpLoginResponse = JSON.parseObject(body, WxMpLoginResponse.class);
              int wxMpLoginResponseErrcode = wxMpLoginResponse.getErrcode();
              if (ObjUtil.isNotEmpty(wxMpLoginResponseErrcode)) {
                  // 微信返回错误
                  if (wxMpLoginResponseErrcode == 40029) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信服务器返回code无效");
                  }
                  if (wxMpLoginResponseErrcode == 45011) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录过于频繁请稍后再试!");
                  }
                  if (wxMpLoginResponseErrcode == 40226) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信高风险等级用户,小程序登录拦截!");
                  }
                  if (wxMpLoginResponseErrcode == -1) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信服务器繁忙请稍后再试");
                  }
              }
              // 返回正常执行登录逻辑
              // 判断该用户是否注册
              String openid = wxMpLoginResponse.getOpenid();
              LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(User::getMpOpenId, openid);
              User user = this.getOne(queryWrapper);
              if (ObjUtil.isEmpty(user)) {
                  user = new User();
                  String randowmAccount = DEFAULT_ACCOUNT_PREFIX + IdUtil.fastSimpleUUID();
                  String passowrdMD5 = DigestUtils.md5DigestAsHex((SALT + DEFAULT_PASSWORD).getBytes());
                  String secretId = generateSecretId(randowmAccount);
                  String secretKey = generateSecretKey(randowmAccount);
                  user.setUserAccount(randowmAccount);
                  user.setUserPassword(passowrdMD5);
                  user.setUserAvatar(apiUrl + DEFAULT_USER_AVATAR);
                  user.setUserRole("user");
                  user.setMpOpenId(openid);
                  user.setUserName(RandomNickName.getRandomName());
                  user.setSecretId(secretId);
                  user.setSecretKey(secretKey);
                  user.setBalanceGoldCoin(DEFAULT_BALANCE_GOLDCOIN);
                  user.setUserProfile(DEFAULT_USER_PROFILE);
                  boolean save = this.save(user);
                  if (!save) {
                      throw new BusinessException(ErrorCode.SYSTEM_ERROR, "登录失败,保存用户信息异常,请稍后再试");
                  }
              }
              // 用户已注册,执行登录逻辑
              LoginUserVO loginUserVO = BeanUtil.copyProperties(user, LoginUserVO.class);
              // 存入缓存
              stringRedisTemplate.opsForValue().set(LOGIN_XCX_SCENE_KEY + scene, user.getId().toString(), LOGIN_XCX_SCENE_TTL, TimeUnit.MINUTES);
              cacheClient.set(LOGIN_TOKEN_KEY + user.getId(), loginUserVO, LOGIN_TOKEN_TTL, TimeUnit.MINUTES);
              return loginUserVO;
          }
      
          @Override
          public LoginUserVO getLoginUserXcxCheck(String scene) {
              // 判断小程序是否已经授权
              String userId = stringRedisTemplate.opsForValue().get(LOGIN_XCX_SCENE_KEY + scene);
              if(StrUtil.isBlank(userId)){
                  log.info("缓存中未查询到微信小程序scene");
                  return null;
              }
              String loginUserVOJson = stringRedisTemplate.opsForValue().get(LOGIN_TOKEN_KEY + userId);
              if (StrUtil.isBlank(loginUserVOJson)) {
                  log.info("缓存中未查询到微信小程序登录信息");
                  return null;
              }
              LoginUserVO currentUser = JSON.parseObject(loginUserVOJson, LoginUserVO.class);
              return currentUser;
          }
      
          @Override
          public LoginXcxQrVO userLoginByXcxCreateQR() {
              String appId = wxMpConfig.getAppId();
              String secret = wxMpConfig.getSecret();
              WxXcxQRUtils wxXcxQRUtils = new WxXcxQRUtils(appId, secret);
              CreateXcxQrRequestBodyRequest createXcxQrRequestBodyRequest = new CreateXcxQrRequestBodyRequest();
              String scene = IdUtil.fastSimpleUUID();
              createXcxQrRequestBodyRequest.setScene(scene);
              createXcxQrRequestBodyRequest.setPage("pages/index/index");
              createXcxQrRequestBodyRequest.setCheck_path(false);
              String qrName = null;
              try {
                  qrName = wxXcxQRUtils.getXcxQRUtils(createXcxQrRequestBodyRequest, DEFAULT_XCX_QRCODE_PATH);
              } catch (IOException e) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "微信小程序二维码生成失败,请刷新页面后再试");
              }
              LoginXcxQrVO loginXcxQrVO = new LoginXcxQrVO();
              loginXcxQrVO.setScene(scene);
              loginXcxQrVO.setQrName(qrName);
      
              return loginXcxQrVO;
          }
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130

# 邮箱登录

很简单,直接调用我们之前写的发送邮件工具类就可以了

    @Override
    public LoginUserVO userLoginByEmail(UserLoginByEmailRequest userLoginByEmailRequest) {
        String email = userLoginByEmailRequest.getEmail();
        String verificationCode = userLoginByEmailRequest.getVerificationCode();
        if (StrUtil.isBlank(email) || StrUtil.isBlank(verificationCode)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号或验证码不能为空");
        }

        // 检查短信验证码是否过期
        String loginCodeInCache = stringRedisTemplate.opsForValue().get(VERIFICATIONCODE_CACHE_KEY + email);
        if (StrUtil.isBlank(loginCodeInCache)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "验证码未发送或已过期,请重新获取");
        }

        // 比对验证码
        if (!loginCodeInCache.equals(verificationCode)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误,请重新输入");
        }

        // 检查手机是否已经注册
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getEmail, email);
        User user = this.getOne(queryWrapper);
        if (ObjUtil.isEmpty(user)) {
            // 用户未注册,执行注册逻辑
            String randowmAccount = DEFAULT_ACCOUNT_PREFIX + IdUtil.fastSimpleUUID();
            String secretId = generateSecretId(randowmAccount);
            String secretKey = generateSecretKey(randowmAccount);

            user = new User();
            String passowrdMD5 = DigestUtils.md5DigestAsHex((SALT + DEFAULT_PASSWORD).getBytes());
            user.setUserAccount(randowmAccount);
            user.setUserPassword(passowrdMD5);
            user.setUserAvatar(apiUrl + DEFAULT_USER_AVATAR);
            user.setEmail(email);
            user.setUserRole("user");
            user.setUserName(RandomNickName.getRandomName());
            user.setSecretId(secretId);
            user.setSecretKey(secretKey);
            user.setBalanceGoldCoin(DEFAULT_BALANCE_GOLDCOIN);
            user.setUserProfile(DEFAULT_USER_PROFILE);
            boolean save = this.save(user);
            if (!save) {
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,请稍后再试");
            }
        }
        // 用户已注册,执行登录逻辑
        LoginUserVO loginUserVO = BeanUtil.copyProperties(user, LoginUserVO.class);
        cacheClient.set(LOGIN_TOKEN_KEY + user.getId(), loginUserVO, LOGIN_TOKEN_TTL, TimeUnit.MINUTES);
        return loginUserVO;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 上线前功能增强

这儿就不附上代码了,源码请看我的github (opens new window)

  1. 代码

# 线程安全问题解决

在我们系统中有增减金额或计数相关的模块,因此一定存在线程安全问题,我们现在来解决

# 问题复现

  1. 修改用户金额为1万

  2. image-20240308151703350

  3. 我们可以看到金额被减去了1078次,而接口调用次数才增加了138次,每个数据都和我们的访问次数对不上

    image-20240308151835748

    image-20240308151748382

# 引入Redission分布式锁

  1. 依赖

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.16.4</version>
    </dependency>
    
    1
    2
    3
    4
    5
  2. config

    package top.panyuwen.gigotapigateway.config;
    
    import lombok.Data;
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * Redisson 配置
     *
     * @author qimu
     */
    @Configuration
    @ConfigurationProperties(prefix = "spring.redis")
    @Data
    public class RedissonConfig {
    
        private String host;
    
        private String port;
    
        @Bean
        public RedissonClient redissonClient() {
            // 1. 创建配置
            Config config = new Config();
            String redisAddress = String.format("redis://%s:%s", host, port);
            config.useSingleServer().setAddress(redisAddress).setDatabase(4);
            // 2. 创建实例
            return Redisson.create(config);
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
  3. Utils

    package top.panyuwen.gigotapigateway.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.stereotype.Component;
    import top.panyuwen.gigotapigateway.common.ErrorCode;
    import top.panyuwen.gigotapigateway.exception.BusinessException;
    
    import javax.annotation.Resource;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Supplier;
    
    /**
     * @Author: PYW
     * @Date: 202/03/08 11:11:26
     * @Version: 1.0
     * @Description: Redisson Lock Utils
     */
    @Slf4j
    @Component
    public class RedissonLockUtil {
    
        @Resource
        public RedissonClient redissonClient;
    
        /**
         * redisson分布式锁
         *
         * @param lockName     锁名称
         * @param supplier     供应商
         * @param errorCode    错误代码
         * @param errorMessage 错误消息
         * @return {@link T}
         */
        public <T> T redissonDistributedLocks(String lockName, Supplier<T> supplier, ErrorCode errorCode, String errorMessage) {
            RLock rLock = redissonClient.getLock(lockName);
            try {
                if (rLock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
                    return supplier.get();
                }
                throw new BusinessException(errorCode.getCode(), errorMessage);
            } catch (Exception e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    log.error("unLock: " + Thread.currentThread().getId());
                    rLock.unlock();
                }
            }
        }
    
        /**
         * redisson分布式锁
         *
         * @param waitTime     等待时间
         * @param leaseTime    租赁时间
         * @param unit         单元
         * @param lockName     锁名称
         * @param supplier     供应商
         * @param errorCode    错误代码
         * @param errorMessage 错误消息
         * @param args         args
         * @return {@link T}
         */
        public <T> T redissonDistributedLocks(long waitTime, long leaseTime, TimeUnit unit, String lockName, Supplier<T> supplier, ErrorCode errorCode, String errorMessage, Object... args) {
            RLock rLock = redissonClient.getLock(lockName);
            try {
                if (rLock.tryLock(waitTime, leaseTime, unit)) {
                    return supplier.get();
                }
                throw new BusinessException(errorCode.getCode(), errorMessage);
            } catch (Exception e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    log.info("unLock: " + Thread.currentThread().getId());
                    rLock.unlock();
                }
            }
        }
    
        /**
         * redisson分布式锁
         *
         * @param unit         时间单位
         * @param lockName     锁名称
         * @param supplier     供应商
         * @param errorCode    错误代码
         * @param errorMessage 错误消息
         * @param time         时间
         * @return {@link T}
         */
        public <T> T redissonDistributedLocks(long time, TimeUnit unit, String lockName, Supplier<T> supplier, ErrorCode errorCode, String errorMessage) {
            RLock rLock = redissonClient.getLock(lockName);
            try {
                if (rLock.tryLock(time, unit)) {
                    return supplier.get();
                }
                throw new BusinessException(errorCode.getCode(), errorMessage);
            } catch (Exception e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    log.info("unLock: " + Thread.currentThread().getId());
                    rLock.unlock();
                }
            }
        }
    
    
        /**
         * redisson分布式锁
         *
         * @param lockName  锁名称
         * @param supplier  供应商
         * @param errorCode 错误代码
         * @return {@link T}
         */
        public <T> T redissonDistributedLocks(String lockName, Supplier<T> supplier, ErrorCode errorCode) {
            return redissonDistributedLocks(lockName, supplier, errorCode, errorCode.getMessage());
        }
    
        /**
         * redisson分布式锁
         *
         * @param lockName     锁名称
         * @param supplier     供应商
         * @param errorMessage 错误消息
         * @return {@link T}
         */
        public <T> T redissonDistributedLocks(String lockName, Supplier<T> supplier, String errorMessage) {
            return redissonDistributedLocks(lockName, supplier, ErrorCode.OPERATION_ERROR, errorMessage);
        }
    
        /**
         * redisson分布式锁
         *
         * @param lockName 锁名称
         * @param supplier 供应商
         * @return {@link T}
         */
        public <T> T redissonDistributedLocks(String lockName, Supplier<T> supplier) {
            return redissonDistributedLocks(lockName, supplier, ErrorCode.OPERATION_ERROR);
        }
    
        /**
         * redisson分布式锁
         *
         * @param lockName     锁名称
         * @param runnable     可运行
         * @param errorCode    错误代码
         * @param errorMessage 错误消息
         */
        public void redissonDistributedLocks(String lockName, Runnable runnable, ErrorCode errorCode, String errorMessage) {
            RLock rLock = redissonClient.getLock(lockName);
            try {
                if (rLock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
                    runnable.run();
                } else {
                    throw new BusinessException(errorCode.getCode(), errorMessage);
                }
            } catch (Exception e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    log.info("lockName:{},unLockId:{} ", lockName, Thread.currentThread().getId());
                    rLock.unlock();
                }
            }
        }
    
        /**
         * redisson分布式锁
         *
         * @param lockName  锁名称
         * @param runnable  可运行
         * @param errorCode 错误代码
         */
        public void redissonDistributedLocks(String lockName, Runnable runnable, ErrorCode errorCode) {
            redissonDistributedLocks(lockName, runnable, errorCode, errorCode.getMessage());
        }
    
        /**
         * redisson分布式锁
         *
         * @param lockName     锁名称
         * @param runnable     可运行
         * @param errorMessage 错误消息
         */
        public void redissonDistributedLocks(String lockName, Runnable runnable, String errorMessage) {
            redissonDistributedLocks(lockName, runnable, ErrorCode.OPERATION_ERROR, errorMessage);
        }
    
        /**
         * redisson分布式锁
         *
         * @param lockName 锁名称
         * @param runnable 可运行
         */
        public void redissonDistributedLocks(String lockName, Runnable runnable) {
            redissonDistributedLocks(lockName, runnable, ErrorCode.OPERATION_ERROR, ErrorCode.OPERATION_ERROR.getMessage());
        }
    
    
        /**
         * redisson分布式锁 可自定义 waitTime 、leaseTime、TimeUnit
         *
         * @param waitTime     等待时间
         * @param leaseTime    租赁时间
         * @param unit         时间单位
         * @param lockName     锁名称
         * @param runnable     可运行
         * @param errorCode    错误代码
         * @param errorMessage 错误消息
         */
        public void redissonDistributedLocks(long waitTime, long leaseTime, TimeUnit unit, String lockName, Runnable runnable, ErrorCode errorCode, String errorMessage) {
            RLock rLock = redissonClient.getLock(lockName);
            try {
                if (rLock.tryLock(waitTime, leaseTime, unit)) {
                    runnable.run();
                } else {
                    throw new BusinessException(errorCode.getCode(), errorMessage);
                }
            } catch (Exception e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    log.info("unLock: " + Thread.currentThread().getId());
                    rLock.unlock();
                }
            }
        }
    
        /**
         * redisson分布式锁 可自定义 time 、unit
         *
         * @param time         时间
         * @param unit         时间单位
         * @param lockName     锁名称
         * @param runnable     可运行
         * @param errorCode    错误代码
         * @param errorMessage 错误消息
         */
        public void redissonDistributedLocks(long time, TimeUnit unit, String lockName, Runnable runnable, ErrorCode errorCode, String errorMessage) {
            RLock rLock = redissonClient.getLock(lockName);
            try {
                if (rLock.tryLock(time, unit)) {
                    runnable.run();
                } else {
                    throw new BusinessException(errorCode.getCode(), errorMessage);
                }
            } catch (Exception e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, e.getMessage());
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    log.info("unLock: " + Thread.currentThread().getId());
                    rLock.unlock();
                }
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262

# 解决

  1. 网关扣减金额和调用次数增加模块修改

                                // 接口调用次数+1
                                try {
                                    redissonLockUtil.redissonDistributedLocks(("gateway_" + userId).intern(), () -> {
    
                                        boolean invoke = innerUserInterfaceInfoService.invoke(interfaceId, userId);
    
                                    }, "接口调用失败");
                                } catch (BusinessException e) {
                                    return Mono.error(new BusinessException(e.getCode(), e.getMessage()));
                                }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  2. 但是这样做还是有问题,我们请求1000次,只成功了10次,这是由于锁一直没有获取到,其他990次都被返回错误了,因此我们需要给锁加一个等待时间(请求数据等待获取锁的时间,超过了返回错误)和租赁时间(获取锁最长的时间,过期销毁,防止死锁问题发生)

                                // 接口调用次数+1
                                try {
                                    redissonLockUtil.redissonDistributedLocks(1000, 10000, TimeUnit.MILLISECONDS,("gateway_" + userId).intern(), () -> {
                                        boolean invoke = innerUserInterfaceInfoService.invoke(interfaceId, userId);
                                    },ErrorCode.OPERATION_ERROR, "接口调用繁忙请稍后再试");
                                } catch (BusinessException e) {
                                    return Mono.error(new BusinessException(e.getCode(), e.getMessage()));
                                }
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. 我们增加了一秒的等待时间和租赁时间,以上修改了之后我们请求1000次成功了567次,成功率大大提升了,但是我们的接口计数是567次扣取金额是568次,数据有出入,估计是方法执行一半出现了问题,因此我们需要给方法加上事务,回到后台管理系统的inner

        @Transactional(rollbackFor = Exception.class)
        @Override
        public boolean invoke(Long interfaceInfoId, Long userId) {
            return userInterfaceInfoService.invoke(interfaceInfoId, userId);
        }
    
    1
    2
    3
    4
    5
  4. 通过检查代码我们发现之前写的代码都是使用的mybatis-plus的LambdaQueryWrapper,这个是无法保证原子性的,因此我们需要把两个加减的地方替换为sql的形式

        @Transactional(rollbackFor = Exception.class)
        @Override
        public boolean invoke(Long id, Long userId) {
            if(ObjUtil.isEmpty(id)){
                throw new BusinessException(ErrorCode.PARAMS_ERROR,"请求参数为空");
            }
            InterfaceInfo interfaceInfoInDB = getById(id);
            if(ObjUtil.isEmpty(interfaceInfoInDB)){
                throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"未找到该数据");
            }
            InterfaceInfo interfaceInfo = new InterfaceInfo();
            interfaceInfo.setId(interfaceInfoInDB.getId());
            interfaceInfo.setTotalInvokes(interfaceInfoInDB.getTotalInvokes()+1);
            // 扣减,注意这个地方需要取负数
            boolean flag = userService.adjustmentGoldCoin(userId, -interfaceInfoInDB.getPayGoldCoin());
            return update().setSql("totalInvokes = totalInvokes + 1").eq("id",interfaceInfo.getId()).update();
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
        @Transactional(rollbackFor = Exception.class)
        @Override
        public boolean adjustmentGoldCoin(Long userId, Long addGoldCoin) {
            // 非空判断
            if (ObjUtil.isEmpty(userId) || ObjUtil.isEmpty(addGoldCoin)) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户id,或者调整金额不能为空!");
            }
            // 判断addGoldCoin是正数还是负数
            if (addGoldCoin < 0L) {
                // 负数
                User user = this.getById(userId);
                if (user.getBalanceGoldCoin() + addGoldCoin < 0) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "余额不足");
                }
            }
            boolean update = update().setSql("balanceGoldCoin = balanceGoldCoin +" + addGoldCoin).eq("id", userId).update();
    
            if (!update) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "调整金币失败");
            }
            return true;
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  5. 现在我们就正常获取了

# 运行分析

# 统计数据

首先先确定好我们的统计数据

  • 收入总额
    • 周同比
    • 日同比
    • 日销售额
  • 支付笔数
    • 待支付
  • 近期接口平均耗时
    • 调用次数
  • 访问量
    • 在线用户
  • 收入统计(近30日)
  • 用户充值排名
  • 接口调用
    • 接口调用(近7天)
    • 最受欢迎接口
  • 接口调用排名
  • 调用次数占比

# 代码

  • SQL

    DROP VIEW IF EXISTS `interface_info_proportion`;
    CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `interface_info_proportion` AS select `ii`.`name` AS `name`,(`ii`.`totalInvokes` / (select sum(`interface_info`.`totalInvokes`) from `interface_info`)) AS `ratio` from `interface_info` `ii`;
    
    -- ----------------------------
    -- View structure for interface_log_week_count
    -- ----------------------------
    DROP VIEW IF EXISTS `interface_log_week_count`;
    CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `interface_log_week_count` AS select `d`.`day` AS `day`,count(`il`.`createTime`) AS `count` from ((select (curdate() - interval (`a`.`a` + (10 * `b`.`a`)) day) AS `day` from ((select 0 AS `a` union all select 1 AS `1` union all select 2 AS `2` union all select 3 AS `3` union all select 4 AS `4` union all select 5 AS `5` union all select 6 AS `6` union all select 7 AS `7`) `a` join (select 0 AS `a` union all select 1 AS `1` union all select 2 AS `2` union all select 3 AS `3` union all select 4 AS `4` union all select 5 AS `5` union all select 6 AS `6` union all select 7 AS `7`) `b`)) `d` left join `interface_log` `il` on((cast(`il`.`createTime` as date) = `d`.`day`))) where (`d`.`day` between (curdate() - interval 6 day) and curdate()) group by `d`.`day` order by `d`.`day`;
    
    -- ----------------------------
    -- View structure for product_order_payout_rank
    -- ----------------------------
    DROP VIEW IF EXISTS `product_order_payout_rank`;
    CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `product_order_payout_rank` AS select `top_7`.`userName` AS `userName`,`top_7`.`total` AS `total`,row_number() OVER (ORDER BY `top_7`.`total` desc )  AS `payOutRank` from (select `u`.`userName` AS `userName`,sum(`po`.`total`) AS `total` from (`product_order` `po` left join `user` `u` on((`po`.`userId` = `u`.`id`))) where (`po`.`status` = 'SUCCESS') group by `po`.`userId` order by `total` desc limit 7) `top_7`;
    
    -- ----------------------------
    -- View structure for product_order_period_introducerow
    -- ----------------------------
    DROP VIEW IF EXISTS `product_order_period_introducerow`;
    CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `product_order_period_introducerow` AS select sum((case when (cast(`product_order`.`createTime` as date) = curdate()) then `product_order`.`total` else 0 end)) AS `day_total`,concat(round((((sum((case when (cast(`product_order`.`createTime` as date) = curdate()) then `product_order`.`total` else 0 end)) / sum((case when (cast(`product_order`.`createTime` as date) = (curdate() - interval 1 day)) then `product_order`.`total` else 0 end))) - 1) * 100),2),'%') AS `day_over_day`,concat(round((((sum((case when (week(`product_order`.`createTime`,0) = week(curdate(),0)) then `product_order`.`total` else 0 end)) / sum((case when (week(`product_order`.`createTime`,0) = week((curdate() - interval 7 day),0)) then `product_order`.`total` else 0 end))) - 1) * 100),2),'%') AS `week_over_week` from `product_order` where (`product_order`.`status` = 'SUCCESS');
    
    -- ----------------------------
    -- View structure for product_order_total_day
    -- ----------------------------
    DROP VIEW IF EXISTS `product_order_total_day`;
    CREATE ALGORITHM = UNDEFINED SQL SECURITY DEFINER VIEW `product_order_total_day` AS select `d`.`day` AS `day`,(case when ((sum(`po`.`total`) <> NULL) or (sum(`po`.`total`) <> '')) then sum(`po`.`total`) else 0 end) AS `total` from ((select (curdate() - interval ((`a`.`a` + (10 * `b`.`a`)) + (100 * `c`.`a`)) day) AS `day` from (((select 0 AS `a` union all select 1 AS `1` union all select 2 AS `2` union all select 3 AS `3` union all select 4 AS `4` union all select 5 AS `5` union all select 6 AS `6` union all select 7 AS `7` union all select 8 AS `8` union all select 9 AS `9`) `a` join (select 0 AS `a` union all select 1 AS `1` union all select 2 AS `2` union all select 3 AS `3` union all select 4 AS `4` union all select 5 AS `5` union all select 6 AS `6` union all select 7 AS `7` union all select 8 AS `8` union all select 9 AS `9`) `b`) join (select 0 AS `a` union all select 1 AS `1` union all select 2 AS `2` union all select 3 AS `3` union all select 4 AS `4` union all select 5 AS `5` union all select 6 AS `6` union all select 7 AS `7` union all select 8 AS `8` union all select 9 AS `9`) `c`)) `d` left join `product_order` `po` on(((cast(`po`.`createTime` as date) = `d`.`day`) and (`po`.`status` = 'SUCCESS')))) where (`d`.`day` between (curdate() - interval 29 day) and curdate()) group by `d`.`day` order by `d`.`day`;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • Controller

    package top.panyuwen.gigotapibackend.controller;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.util.ObjectUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import top.panyuwen.gigotapibackend.annotation.AuthCheck;
    import top.panyuwen.gigotapibackend.common.BaseResponse;
    import top.panyuwen.gigotapibackend.common.ResultUtils;
    import top.panyuwen.gigotapibackend.constant.UserConstant;
    import top.panyuwen.gigotapibackend.service.*;
    import top.panyuwen.gigotapicommon.model.entity.*;
    import top.panyuwen.gigotapicommon.model.enums.ProductOrderStatusEnum;
    import top.panyuwen.gigotapicommon.model.vo.analysis.*;
    
    import javax.annotation.Resource;
    
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    import static top.panyuwen.gigotapibackend.constant.RedisConstants.LOGIN_TOKEN_KEY;
    import static top.panyuwen.gigotapibackend.constant.RedisConstants.SYSTEM_PV_KEY;
    
    /**
     * 运行分析控制器
     *
     * @author PYW
     * @from www.panyuwen.top
     */
    @RestController
    @RequestMapping("/analysis")
    @Slf4j
    public class AnalyseController {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Resource
        private InterfaceInfoService interfaceInfoService;
    
        @Resource
        private UserService userService;
    
        @Resource
        private InterfaceLogService interfaceLogService;
    
        @Resource
        private ProductOrderService productOrderService;
    
        @Resource
        private ProductOrderTotalDayService productOrderTotalDayService;
    
        @Resource
        private ProductOrderPayoutRankService productOrderPayoutRankService;
    
        @Resource
        private ProductOrderPeriodIntroducerowService productOrderPeriodIntroducerowService;
    
        @Resource
        private InterfaceLogWeekCountService interfaceLogWeekCountService;
    
        @Resource
        private InterfaceInfoProportionService interfaceInfoProportionService;
    
        @GetMapping("/interface/introduceRow")
        public BaseResponse<IntroduceRowVO> getInterfaceIntroduceRow() {
    
            IntroduceRowVO introduceRowVO = new IntroduceRowVO();
    
            // 获取接口调用次数
            Long interfaceInfoCount = interfaceInfoService.getInterfaceInfoTotalInvokesCount();
            // 获取最近接口调用平均时间
            Integer cost = interfaceLogService.getInterfaceInfoAverageCost();
            introduceRowVO.setInterfaceInfoCount(interfaceInfoCount);
            introduceRowVO.setCost(cost);
            return ResultUtils.success(introduceRowVO);
        }
    
        @GetMapping("/introduceRow")
        @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
        public BaseResponse<IntroduceRowVO> getIntroduceRow() {
            Long size = stringRedisTemplate.opsForHyperLogLog().size(SYSTEM_PV_KEY);
            // 获取PV
            IntroduceRowVO introduceRowVO = new IntroduceRowVO();
            introduceRowVO.setPv(size);
    
            // 用户数
            Set<String> keys = stringRedisTemplate.keys( LOGIN_TOKEN_KEY + "*");
            if (ObjectUtil.isEmpty(keys)) {
                introduceRowVO.setOnLineUserCount(0L);
            }
            introduceRowVO.setOnLineUserCount(Long.valueOf(keys.size()));
    
            // 获取接口调用次数
            Long interfaceInfoCount = interfaceInfoService.getInterfaceInfoTotalInvokesCount();
            // 获取最近接口调用平均时间
            Integer cost = interfaceLogService.getInterfaceInfoAverageCost();
            introduceRowVO.setInterfaceInfoCount(interfaceInfoCount);
            introduceRowVO.setCost(cost);
    
            // 获取支付笔数
            Long successCount = productOrderService.getCountByStatus(ProductOrderStatusEnum.SUCCESS);
            introduceRowVO.setSuccessPayCount(successCount);
    
            // 未支付订单数
            Long noPayCount = productOrderService.getCountByStatus(ProductOrderStatusEnum.NOTPAY);
            introduceRowVO.setNoPayCount(noPayCount);
    
            // 总收入金额
            Long sucessTotalAmount = productOrderService.getSucessTotalAmount();
            introduceRowVO.setSucessTotalAmount(sucessTotalAmount);
    
            // 获取周期介绍
            ProductOrderPeriodIntroducerow productOrderPeriodIntroducerow = productOrderPeriodIntroducerowService.list().get(0);
            Long dayTotal = productOrderPeriodIntroducerow.getDayTotal();
            String dayOverDay = productOrderPeriodIntroducerow.getDayOverDay();
            String weekOverWeek = productOrderPeriodIntroducerow.getWeekOverWeek();
            introduceRowVO.setDayTotal(dayTotal);
            introduceRowVO.setDayOverDay(dayOverDay);
            introduceRowVO.setWeekOverWeek(weekOverWeek);
    
            return ResultUtils.success(introduceRowVO);
        }
    
        @GetMapping("/salesCard")
        @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
        public BaseResponse<SalesCardVO> getSalesCard(){
            List<ProductOrderTotalDay> productOrderTotalDayList = productOrderTotalDayService.list();
            List<ProductOrderPayoutRank> productOrderPayoutRankList = productOrderPayoutRankService.list();
            SalesCardVO salesCardVO = new SalesCardVO();
            salesCardVO.setTotalDay(productOrderTotalDayList);
            salesCardVO.setPayoutRank(productOrderPayoutRankList);
            return ResultUtils.success(salesCardVO);
        }
    
        @GetMapping("/topInterfaceInfo")
        @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
        public BaseResponse<TopInterfaceInfoVO> getTopInterfaceInfo() {
            TopInterfaceInfoVO topInterfaceInfoVO = new TopInterfaceInfoVO();
            List<InterfaceLogWeekCount> interfaceLogWeekCountList = interfaceLogWeekCountService.list();
            List<InterfaceInfo> interfaceInfoList = interfaceInfoService.list();
            List<InterfaceInfoTotalCountVO> interfaceInfoTotalCountVOList = interfaceInfoList.stream().map(
                    interfaceInfo -> BeanUtil.copyProperties(interfaceInfo, InterfaceInfoTotalCountVO.class)
            ).collect(Collectors.toList());
            topInterfaceInfoVO.setInterfaceInfoTotalCount(interfaceInfoTotalCountVOList);
            topInterfaceInfoVO.setInterfaceLogWeekCounts(interfaceLogWeekCountList);
            topInterfaceInfoVO.setMostPopular(interfaceInfoList.get(0).getName());
            return ResultUtils.success(topInterfaceInfoVO);
        }
    
        @GetMapping("/interfaceinfoProportion")
        @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
        public BaseResponse<ProportionSalesVO> getInterfaceinfoProportion() {
            ProportionSalesVO proportionSalesVO = new ProportionSalesVO();
            List<InterfaceInfoProportion> interfaceInfoProportionList = interfaceInfoProportionService.list();
            proportionSalesVO.setInterfaceInfoProportionList(interfaceInfoProportionList);
            return ResultUtils.success(proportionSalesVO);
        }
    
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165

# 上线部署+

3

# 安装

  • 安装nacos

    systemctl start nacos
    
    1
  • 安装redis

  • jdk

# 上线

# 文档(gigotapi-doc)

  • 构建静态文件

    vuepress build
    
    1
  • 部署静态文件到服务器,静态资源文件目录:gigotapi-doc\docs.vuepress\dist

  • 腾讯云配置域名解析

    image-20240311114057449

  • 配置nginx反向代理

    server
    {
            listen 80;
            server_name doc.panyuwen.top;
    
            location / {
                proxy_pass http://doc.panyuwen.top:7002;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 在宝塔上配置一个静态资源项目

  • 开放端口,注意宝塔和云服务器都需要开启

  • 上线成功✔

    image-20240311113447865

# 前端(gigotapi-fontend)

  • 构建静态资源文件,我们使用的ant design pro很简单直接使用build就可以了

    image-20240311113724527

  • 构建后会生成一个dist,里面就是静态文件的代码

    image-20240311113816679

  • 上传到服务器

  • 腾讯云配置域名解析

  • nginx反向代理

    server
    {
            listen 80;
            server_name api.panyuwen.top;
    
            location / {
                proxy_pass http://api.panyuwen.top:7001;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 在宝塔上配置一个静态资源项目

  • 开放端口,注意宝塔和云服务器都需要开启

  • 申请一个ssl证书用于https访问,ohttps申请ssl证书

  • 上线成功✔

    image-20240311114322671

image-20240312234503433

# 后端

  • 开放平台后端(gigotapi-backend)

    • 新增一个prod正式环境的yml,修改信息为正式环境
      • mysql
      • redis
      • 端口号
      • nacos
      • 微信支付回调地址,私钥文件夹
      • 微信公众平台appId,secret
  • 快速接入sdk(gigotapi-client-sdk)

    • 修改网关为正式地址
  • 上传jar包

  • 宝塔配置java项目并使用prod启动

    /www/server/java/jdk1.8.0_371/bin/java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=6778 -Dcom.sun.management.jmxremote.port=6187 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=127.0.0.1 -jar -Xmx1024M -Xms256M  /www/wwwroot/gigot-api/gigotapi/gigotapi-backend/gigotapi-backend-0.0.1.jar --server.port=8001 --spring.profiles.active=prod
    
    1
  • 配置前端的nginx配置

    image-20240312184846221

  • 成功访问✔

# 踩坑记录

  • 太坑了,宝塔开启https后会窜站,我就说我的doc为什么一直访问到api前端,

    • 解决方案:宝塔开启防窜站功能
  • 前端点击按钮跳转没问题,但是一旦使用了history跳转或者刷新页面就会显示404,

    • **解决方案:**在配置中添加

      error_page 404  /index.html;
      
      1
  • 服务器内存太小只有2个G,服务已启动nacos就崩

    • **解决方案:**修改每个程序和nacos的最大运行内存为256最小运行内存为128

# 启动

# 本地启动步骤

  1. 启动mysql数据库(net start mysql80)
  2. 启动redis数据库
  3. 如需测试微信支付,需要开启内网穿透工具ngrok,并修改yml文件中的接收支付结果地址(wx.pay.notify-domain)(ngrok http 项目端口号)
  4. 启动nacos(该项目使用2.1.1版本)
  5. 启动gigotapi-backend,必须先启动这个,因为gateway网关的dubbo的生产者是gigotapi-backend而gateway网关是消费者
  6. 启动gigotapi-gateway网关,gigotapi-interface

# 服务器启动步骤

  1. mysql

  2. docker(redis)

  3. 前端项目(gigotapi-fontend,gigotapi-doc)

  4. nacos

    sh /opt/nacos-2.1.1/bin/ startup.sh -m standalone
    
    1
  5. 后端项目gigotapi-backend,必须先启动这个,因为gateway网关的dubbo的生产者是gigotapi-backend而gateway网关是消费者,启动gigotapi-gateway网关,gigotapi-interface

注意:因为服务器内存较小只有2G,并且使用了nacos内存很容易占满,启动时需要检查内存

# 开发中的奇思妙想(待完善)

# 接口日志统计信息

API接口调用的记录通常包括以下信息:

  1. 调用时间:记录每次调用API的时间,方便后续分析和定位问题。 ✔
  2. 请求方法:GET、POST、PUT、DELETE等请求方法。 ✔
  3. 请求URL:调用的API接口URL。 ✔
  4. 响应时间:API接口返回响应的时间。 ✔
  5. 响应状态码:API接口返回的HTTP状态码。 ✔
  6. 调用次数:记录每次调用API的次数,可以统计API的调用频率和使用情况。 ✔
  7. 调用成本:记录API的请求和响应流量(通过请求头和响应体实现)✔

# 添加订单功能

  1. 订单表添加
  2. 每个接口使用多少积分可以调用
  3. 绑定邮箱,发送订单信息

# 接口信息表待补全字段:

  1. 积分✔
  2. 接口参数param✔
  3. 点赞

# 金币

  1. 金币表建立
  2. 金币和现金兑换表

# 个人中心

后续还需要添加钱包,下载SDK等功能

# 查看接口文档

  1. 使用博客工具如hexo等快速开发一个在线的api文档类似ant design pro的文档一样

# 网关

  1. 黑名单功能添加,用于计数,比如十秒钟以内请求上万次就加入黑名单,然后在网关的过滤器中直接返回这些用户的请求

# 统计

  • 更新日志

  • 服务监控

  • 在线用户,增加一个强制下线功能

  • 定时任务

  • 每个请求调用次数

ip属地,判断从哪个城市调用的

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;

public class IPQuery {
    public static void main(String[] args) {
        try {
            String ak = "your_baidu_api_key"; // 你的百度API密钥
            String ip = "8.8.8.8"; // 要查询的IP地址
            URL url = new URL("http://api.map.baidu.com/location/ip?ak=" + ak + "&ip=" + ip);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");

            Scanner scanner = new Scanner(connection.getInputStream());
            StringBuilder response = new StringBuilder();

            while (scanner.hasNext()) {
                response.append(scanner.next());
            }

            scanner.close();

            System.out.println(response.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 🎉完结撒花

帮助我们改善此页面! (opens new window)
上次更新: 2024/03/29, 18:47:19
Theme by Vdoing | Copyright © 2024-2024 PYW | 渝ICP备2023001069号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式