当前位置: 首页 > news >正文

Spring Boot实现接口幂等

Spring Boot实现接口幂等

1、pom依赖

<?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><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.6</version><relativePath/></parent><groupId>com.example</groupId><artifactId>idempotent_demo</artifactId><version>0.0.1-SNAPSHOT</version><name>idempotent_demo</name><description>idempotent_demo</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--springboot data redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- StringUtils工具类 --><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.5</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.25</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

2、Redis工具类

package com.example.idempotent_demo.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.concurrent.TimeUnit;/*** @author tom* Redis工具类*/
@Slf4j
@Component
public class RedisUtil {private StringRedisTemplate stringRedisTemplate;@Autowiredpublic void setRedisTemplate(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 将key和value存入redis** @param key        redis的key* @param value      redis的value* @param expireTime key过期时间* @return 保存进redis是否成功*/public boolean save(String key, String value, Long expireTime) {try {// 存储Token到Redis,且设置过期时间为5分钟stringRedisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 验证key和value并删除key** @param key   redis的key* @param value redis的value* @return 验证是否成功*/public boolean valid(String key, String value) {// 设置Lua脚本,其中KEYS[1]是key,KEYS[2]是valueString script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);// 执行Lua脚本Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, value));// 根据返回结果判断是否成功成功匹配并删除Redis键值对,若果结果不为空和0,则验证通过if (null != result && result != 0L) {log.info("验证 key={},value={} 成功", key, value);return true;}log.error("验证 key={},value={} 失败", key, value);return false;}
}

3、Token服务类

token 服务,里面主要是两个方法,一个用来创建 token,一个用来验证 token。

package com.example.idempotent_demo.service;import javax.servlet.http.HttpServletRequest;/*** @author tom*/
public interface TokenService {/*** 创建token** @return*/String generateToken();/*** 检验token** @param request* @return*/boolean validToken(HttpServletRequest request);}
package com.example.idempotent_demo.service.impl;import com.example.idempotent_demo.constant.Constant;
import com.example.idempotent_demo.exception.NoTokenException;
import com.example.idempotent_demo.exception.ValidTokenException;
import com.example.idempotent_demo.service.TokenService;
import com.example.idempotent_demo.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;
import java.util.UUID;/*** @author tom*/
@Service
@Slf4j
public class TokenServiceImpl implements TokenService {private RedisUtil redisUtil;@Autowiredpublic void setRedisUtil(RedisUtil redisUtil) {this.redisUtil = redisUtil;}/*** 创建token** @return*/@Overridepublic String generateToken() {// 实例化生成ID工具对象String uuid = UUID.randomUUID().toString();String token = Constant.IDEMPOTENT_TOKEN_PREFIX + uuid;boolean success = redisUtil.save(token, token, 5L);if (success) {log.info("save token {} to redis success", token);return token;}log.error("save token {} to redis fail", token);return null;}/*** 检验token** @param request* @return*/@Overridepublic boolean validToken(HttpServletRequest request) {String token = request.getHeader(Constant.IDEMPOTENT_TOKEN_HEADER);// header中不存在tokenif (StringUtils.isBlank(token)) {log.error("用户未携带token!");throw new NoTokenException();}// 验证token失败if (!redisUtil.valid(token, token)) {log.error("重复提交!");throw new ValidTokenException();}return true;}
}

redis.get(token) 、token.equals 、redis.del(token) 如果这几个操作不是原子,可能导致,高并发下,都get到同

样的数据,判断都成功,继续业务并发执行。这里 redis 使用 lua 脚本完成这个操作:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

package com.example.idempotent_demo.exception;/*** 用户为携带token* @author tom*/
public class NoTokenException extends RuntimeException {public NoTokenException() {super();}
}
package com.example.idempotent_demo.exception;/*** @author* 验证token失败*/
public class ValidTokenException extends RuntimeException{public ValidTokenException(){super();}
}
package com.example.idempotent_demo.util;/*** @author 结果集返回封装*/
public class ResponseResult {/*** 响应业务状态*/private Integer code;/*** 响应消息*/private String msg;/*** 响应中的数据*/private Object data;public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Object getData() {return data;}public void setData(Object data) {this.data = data;}/*** 无参构造方法*/public ResponseResult() {}/*** 全参构造方法** @param code* @param msg* @param data*/public ResponseResult(Integer code, String msg, Object data) {this.code = code;this.msg = msg;this.data = data;}}
package com.example.idempotent_demo.constant;/*** @author tom*/
public class Constant {/*** 存入Redis的Token键的前缀*/public static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";/*** 请求头的token名称*/public static final String IDEMPOTENT_TOKEN_HEADER = "idempotent_token";
}

4、Redis配置

spring:redis:ssl: falsehost: 127.0.0.1port: 6379database: 0timeout: 1000password:lettuce:pool:max-active: 100max-wait: -1min-idle: 0max-idle: 20
server:servlet:encoding:charset: UTF-8

5、自定义注解

自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现

自动幂等。

后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解 ElementType.METHOD 表示它

只能放在方法上,EetentionPolicy.RUNTIME 表示它在运行时。

package com.example.idempotent_demo.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author tom*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

6、拦截器配置

主要的功能是拦截扫描到 AutoIdempotent 注解的方法,然后调用 TokenService 的 validToken方法校验 token

是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

package com.example.idempotent_demo.config;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.idempotent_demo.annotation.AutoIdempotent;
import com.example.idempotent_demo.exception.NoTokenException;
import com.example.idempotent_demo.exception.ValidTokenException;
import com.example.idempotent_demo.service.TokenService;
import com.example.idempotent_demo.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;/*** @author tom*/
@Slf4j
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {private TokenService tokenService;@Autowiredpublic void setTokenService(TokenService tokenService) {this.tokenService = tokenService;}/*** 预处理** @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();// 被AutoIdempotent注解标记的方法AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);if (methodAnnotation != null) {try {// 幂等性校验,校验通过则放行,校验失败则抛出异常,并通过统一异常处理返回友好提示return tokenService.validToken(request);} catch (NoTokenException ex) {log.error("用户未携带token!");returnJson(response, JSON.toJSONString(new ResponseResult(10001, "用户未携带token!", null), SerializerFeature.WriteMapNullValue));return false;} catch (ValidTokenException ex) {log.error("重复提交!");returnJson(response, JSON.toJSONString(new ResponseResult(10002, "重复提交!", null), SerializerFeature.WriteMapNullValue));return false;}}//必须返回true,否则会被拦截一切请求return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}private void returnJson(HttpServletResponse response, String json) {PrintWriter writer = null;response.setCharacterEncoding("UTF-8");response.setContentType("application/json; charset=utf-8");try {writer = response.getWriter();writer.print(json);} catch (IOException e) {e.printStackTrace();} finally {if (writer != null) {writer.close();}}}
}

7、注册拦截器

添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,

这样在容器启动是时候就可以添加进入context中。

package com.example.idempotent_demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** @author*/
@Configuration
public class WebConfiguration implements WebMvcConfigurer {@Resourceprivate AutoIdempotentInterceptor autoIdempotentInterceptor;/*** 添加拦截器** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(autoIdempotentInterceptor);}
}

8、启动类

package com.example.idempotent_demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @author tom*/
@SpringBootApplication
public class IdempotentDemoApplication {public static void main(String[] args) {SpringApplication.run(IdempotentDemoApplication.class, args);}}

9、测试

9.1 生成token

在这里插入图片描述

请求生成了 token。

9.2 redis查看生成的token

在这里插入图片描述

redis 中生成了 token。

9.3 无header请求

在这里插入图片描述

请求需要携带token。

9.4 正常请求

在这里插入图片描述

请求成功。

9.5 再次查看redis

在这里插入图片描述

发现该 token 已经被删除了。

9.5 再次请求

在这里插入图片描述

返回重复请求。

相关文章:

Spring Boot实现接口幂等

Spring Boot实现接口幂等 1、pom依赖 <?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:…...

ShopsN commentUpload 文件上传漏洞复现

0x01 产品简介 ShopsN 是一款符合企业级商用标准全功能的真正允许免费商业用途的开源网店全网系统。 0x02 漏洞概述 ShopsN commentUpload 接口处存在任意文件上传漏洞,攻击者可以利用文件上传漏洞执行恶意代码、写入后门、读取敏感文件,从而可能导致服务器受到攻击并被控…...

【Qt5】ui文件最后会变成头文件

2023年12月14日&#xff0c;周四下午 我也是今天下午偶然间发现这个的 在使用Qt的uic&#xff08;User Interface Compiler&#xff09;工具编译ui文件时&#xff0c;会生成对应的头文件。 在Qt中&#xff0c;ui文件是用于描述用户界面的XML文件&#xff0c;而头文件是用于在…...

数组笔试题解析(下)

数组面试题解析 字符数组 &#xff08;一&#xff09; 我们上一篇文章学习了一维数组的面试题解析内容和字符数组的部分内容&#xff0c;我们这篇文章讲解一下字符数组和指针剩余面试题的解析内容&#xff0c;那现在&#xff0c;我们开始吧。 我们继续看一组字符数组的面试…...

PPT插件-好用的插件-图形缩放-大珩助手

图形缩放 包括适合屏幕、适合宽度、适合高度、水平翻转、垂直翻转、指定角度&#xff0c;可同时对多个形状进行操作 适合屏幕 一键设置图像、文本、形状的长宽尺寸与当前幻灯片一致 适合宽度 一键设置图像、文本、形状的宽度尺寸与当前幻灯片一致 适合高度 一键设置图像…...

五:爬虫-数据解析之xpath解析

五&#xff1a;数据解析之xpath解析 1.xpath介绍&#xff1a; ​ xpath是XML路径语言&#xff0c;它可以用来确定xml文档中的元素位置&#xff0c;通过元素路径来完成对元素的查找&#xff0c;HTML就是XML的一种实现方式&#xff0c;所以xpath是一种非常强大的定位方式​ XPa…...

什么是Laravel?它有哪些特性?

Laravel 是一款流行的 PHP Web 框架&#xff0c;设计用于构建现代、优雅且功能强大的 Web 应用程序。它提供了一套丰富的工具和库&#xff0c;以简化常见的开发任务&#xff0c;同时保持灵活性和可扩展性。以下是 Laravel 框架的一些主要特性&#xff1a; 优雅的语法&#xff1…...

[足式机器人]Part2 Dr. CAN学习笔记-自动控制原理Ch1-3燃烧卡路里-系统分析实例

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-自动控制原理Ch1-3燃烧卡路里-系统分析实例 1. 数学模型2. 比例控制 Proprotional Control 1. 数学模型 2. 比例控制 Proprotional Control...

安恒明御安全网关 aaa_local_web_preview文件上传漏洞复现

0x01 产品简介 明御安全网关秉持安全可视、简单有效的理念,以资产为视角,构建全流程防御的下一代安全防护体系,并融合传统防火墙、入侵检测、入侵防御系统、防病毒网关、上网行为管控、VPN网关、威胁情报等安全模块于一体的智慧化安全网关。 0x02 漏洞概述 明御安全网关在…...

基于ssm企业人事管理系统的设计与实现论文

摘 要 进入信息时代以来&#xff0c;很多数据都需要配套软件协助处理&#xff0c;这样可以解决传统方式带来的管理困扰。比如耗时长&#xff0c;成本高&#xff0c;维护数据困难&#xff0c;数据易丢失等缺点。本次使用数据库工具MySQL和编程技术SSM开发的企业人事管理系统&am…...

你知道为什么要加 final 关键字了吗?

​ ​嗨&#xff0c;大家好&#xff0c;欢迎来到程序猿漠然公众号&#xff0c;我是漠然。 在Java编程中&#xff0c;我们经常会遇到需要使用final关键字的情况。那么&#xff0c;为什么要使用final关键字呢&#xff1f;它到底有什么作用&#xff1f;本文将从以下几个方面来详细…...

找不到mfc100u.dll,程序无法继续执行?三步即可搞定

在使用电脑过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到mfc100u.dll”。mfc100u.dll是Microsoft Foundation Class&#xff08;MFC&#xff09;库中的一个版本特定的DLL文件。MFC是微软公司为简化Windows应用程序开发而提供的一套C类库。它包…...

postman接口测试之Postman配置环境变量和全局变量

前言  我们在测试的过程中&#xff0c;遇到最多的问题也可以是环境的问题了吧&#xff0c;今天开发用了这个测试环境&#xff0c;明天又换了另一个测试环境&#xff0c;这样对于我们测试非常的麻烦&#xff0c;特别最接口的时候需要来回的输入环境地址比较麻烦&#xff0c;今天…...

OpenSSL 编程示例

参考&#xff1a;深入探索 OpenSSL&#xff1a;概念、原理、开发步骤、使用方法、使用场景及代码示例 地址&#xff1a;https://oneisall.blog.csdn.net/article/details/131489812?spm1001.2014.3001.5502 目录 1. OpenSSL 概念2. OpenSSL 原理3. OpenSSL 开发步骤4. OpenSSL…...

K8S学习指南(17)-k8s核心对象CronJob

文章目录 前言什么是CronJob&#xff1f;示例演示步骤1&#xff1a;创建CronJob步骤2&#xff1a;定义任务模板步骤3&#xff1a;部署CronJob步骤4&#xff1a;监视CronJob的执行 总结 前言 Kubernetes&#xff08;简称K8s&#xff09;是一种用于自动部署、扩展和管理容器化应…...

单片机Freertos入门(二)任务调度的介绍

简介&#xff1a; FreeRTOS支持的任务调度方法有抢占式、协作式、时间片轮转&#xff0c;下面分别来讲解。 1.抢占式调度 抢占式调度&#xff0c;是最高优先级的任务一旦就绪&#xff0c;总能得到CPU的执行权。 高优先级运行时候&#xff0c;低优先级不运行&#xff0c;等待…...

QT----自定义信号和槽

第二天 2.1自定义信号和槽 新建一个Qtclass 自定义信号&#xff1a;返回值是void &#xff0c;只需要声明&#xff0c;不需要实现&#xff0c;可以有参数&#xff0c;可以重载 自定义槽&#xff1a;返回值void &#xff0c;需要声明&#xff0c;也需要实现&#xff0c;可以有…...

【Vue第4章】Vue中的ajax_Vue2

目录 4.1 解决开发环境Ajax跨域问题 4.1.1 解决跨域的三种方法 4.1.2 使用代理服务器 4.1.3 笔记与代码 4.1.3.1 笔记 4.1.3.2 19_src_配置代理服务器 4.2 github用户搜索案例 4.2.1 效果 4.2.2 接口地址 4.2.3 笔记与代码 4.2.3.1 20_src_github搜索案例 4.3 vue项…...

力扣labuladong——一刷day72

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、力扣109. 有序链表转换二叉搜索树二、力扣1382. 将二叉搜索树变平衡 前言 二叉树的递归分为「遍历」和「分解问题」两种思维模式&#xff0c;这道题需要用到…...

Leetcode—509.斐波那契数【简单】

2023每日刷题&#xff08;五十七&#xff09; Leetcode—509.斐波那契数 实现代码 int fib(int n){if(n 0) {return 0;}if(n 1) {return 1;}return fib(n-1) fib(n-2); }运行结果 之后我会持续更新&#xff0c;如果喜欢我的文章&#xff0c;请记得一键三连哦&#xff0c;点…...

stm32G473的flash模式是单bank还是双bank?

今天突然有人stm32G473的flash模式是单bank还是双bank&#xff1f;由于时间太久&#xff0c;我真忘记了。搜搜发现&#xff0c;还真有人和我一样。见下面的链接&#xff1a;https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

【算法训练营Day07】字符串part1

文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接&#xff1a;344. 反转字符串 双指针法&#xff0c;两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

数据库分批入库

今天在工作中&#xff0c;遇到一个问题&#xff0c;就是分批查询的时候&#xff0c;由于批次过大导致出现了一些问题&#xff0c;一下是问题描述和解决方案&#xff1a; 示例&#xff1a; // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

3-11单元格区域边界定位(End属性)学习笔记

返回一个Range 对象&#xff0c;只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意&#xff1a;它移动的位置必须是相连的有内容的单元格…...

Go语言多线程问题

打印零与奇偶数&#xff08;leetcode 1116&#xff09; 方法1&#xff1a;使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...

五子棋测试用例

一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏&#xff0c;有着深厚的文化底蕴。通过将五子棋制作成网页游戏&#xff0c;可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家&#xff0c;都可以通过网页五子棋感受到东方棋类…...