功能实现方案
[toc]
0、资料
订单相关:
限流相关:
其他:
1、热搜+不雅词过滤
使用java和redis实现一个简单的热搜功能,具备以下功能:
- 搜索栏展示当前登陆的个人用户的搜索历史记录,删除个人历史记录
- 用户在搜索栏输入某字符,则将该字符记录下来 以zset格式存储的redis中,记录该字符被搜索的个数以及当前的时间戳 (用了DFA算法,感兴趣的自己百度学习吧)
- 每当用户查询了已在redis存在了的字符时,则直接累加个数, 用来获取平台上最热查询的十条数据。(可以自己写接口或者直接在redis中添加一些预备好的关键词)
- 最后还要做不雅文字过滤功能。这个很重要不说了你懂的。
代码实现热搜与个人搜索记录功能,主要controller层下几个方法就行了 :
- 向redis 添加热搜词汇(添加的时候使用下面不雅文字过滤的方法来过滤下这个词汇,合法再去存储
- 每次点击给相关词热度 +1
- 根据key搜索相关最热的前十名
- 插入个人搜索记录
- 查询个人搜索记录
1.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 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
| import com.jianlet.service.user.RedisService; import org.apache.commons.lang.StringUtils; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.TimeUnit;
@Transactional @Service("redisService") public class RedisServiceImpl implements RedisService { @Resource(name = "redisSearchTemplate") private StringRedisTemplate redisSearchTemplate; @Override public int addSearchHistoryByUserId(String userid, String searchkey) { String shistory = RedisKeyUtils.getSearchHistoryKey(userid); boolean b = redisSearchTemplate.hasKey(shistory); if (b) { Object hk = redisSearchTemplate.opsForHash().get(shistory, searchkey); if (hk != null) { return 1; }else{ redisSearchTemplate.opsForHash().put(shistory, searchkey, "1"); } }else{ redisSearchTemplate.opsForHash().put(shistory, searchkey, "1"); } return 1; } @Override public Long delSearchHistoryByUserId(String userid, String searchkey) { String shistory = RedisKeyUtils.getSearchHistoryKey(userid); return redisSearchTemplate.opsForHash().delete(shistory, searchkey); } @Override public List<String> getSearchHistoryByUserId(String userid) { List<String> stringList = null; String shistory = RedisKeyUtils.getSearchHistoryKey(userid); boolean b = redisSearchTemplate.hasKey(shistory); if(b){ Cursor<Map.Entry<Object, Object>> cursor = redisSearchTemplate.opsForHash().scan(shistory, ScanOptions.NONE); while (cursor.hasNext()) { Map.Entry<Object, Object> map = cursor.next(); String key = map.getKey().toString(); stringList.add(key); } return stringList; } return null; } @Override public int incrementScoreByUserId(String searchkey) { Long now = System.currentTimeMillis(); ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet(); ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue(); List<String> title = new ArrayList<>(); title.add(searchkey); for (int i = 0, lengh = title.size(); i < lengh; i++) { String tle = title.get(i); try { if (zSetOperations.score("title", tle) <= 0) { zSetOperations.add("title", tle, 0); valueOperations.set(tle, String.valueOf(now)); } } catch (Exception e) { zSetOperations.add("title", tle, 0); valueOperations.set(tle, String.valueOf(now)); } } return 1; } @Override public List<String> getHotList(String searchkey) { String key = searchkey; Long now = System.currentTimeMillis(); List<String> result = new ArrayList<>(); ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet(); ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue(); Set<String> value = zSetOperations.reverseRangeByScore("title", 0, Double.MAX_VALUE); if(StringUtils.isNotEmpty(searchkey)){ for (String val : value) { if (StringUtils.containsIgnoreCase(val, key)) { if (result.size() > 9) { break; } Long time = Long.valueOf(valueOperations.get(val)); if ((now - time) < 2592000000L) { result.add(val); } else { zSetOperations.add("title", val, 0); } } } }else{ for (String val : value) { if (result.size() > 9) { break; } Long time = Long.valueOf(valueOperations.get(val)); if ((now - time) < 2592000000L) { result.add(val); } else { zSetOperations.add("title", val, 0); } } } return result; } @Override public int incrementScore(String searchkey) { String key = searchkey; Long now = System.currentTimeMillis(); ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet(); ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue(); zSetOperations.incrementScore("title", key, 1); valueOperations.getAndSet(key, String.valueOf(now)); return 1; } }
|
1.2、配置类
在配置类中,读取Resource/static/censorword.txt
敏感词文件。
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
| import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import java.io.*; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set;
@Configuration @SuppressWarnings({ "rawtypes", "unchecked" }) public class SensitiveWordInit { private String ENCODING = "UTF-8"; public Map initKeyWord() throws IOException { Set<String> wordSet = readSensitiveWordFile(); return addSensitiveWordToHashMap(wordSet); } private Set<String> readSensitiveWordFile() throws IOException { Set<String> wordSet = null; ClassPathResource classPathResource = new ClassPathResource("static/censorword.txt"); InputStream inputStream = classPathResource.getInputStream(); try { InputStreamReader read = new InputStreamReader(inputStream, ENCODING); wordSet = new HashSet<String>(); BufferedReader br = new BufferedReader(read); String txt = null; while ((txt = br.readLine()) != null) { wordSet.add(txt); } br.close(); read.close(); } catch (Exception e) { e.printStackTrace(); } return wordSet; } private Map addSensitiveWordToHashMap(Set<String> wordSet) { Map wordMap = new HashMap(wordSet.size()); for (String word : wordSet) { Map nowMap = wordMap; for (int i = 0; i < word.length(); i++) { char keyChar = word.charAt(i); Object tempMap = nowMap.get(keyChar); if (tempMap != null) { nowMap = (Map) tempMap; } else { Map<String, String> newMap = new HashMap<String, String>(); newMap.put("isEnd", "0"); nowMap.put(keyChar, newMap); nowMap = newMap; } if (i == word.length() - 1) { nowMap.put("isEnd", "1"); } } } return wordMap; } }
|
1.3、工具类
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
| import java.io.IOException; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set;
public class SensitiveFilter { private Map sensitiveWordMap = null; public static int minMatchType = 1; public static int maxMatchType = 2; private static SensitiveFilter instance = null; private SensitiveFilter() throws IOException { sensitiveWordMap = new SensitiveWordInit().initKeyWord(); } public static SensitiveFilter getInstance() throws IOException { if (null == instance) { instance = new SensitiveFilter(); } return instance; } public Set<String> getSensitiveWord(String txt, int matchType) { Set<String> sensitiveWordList = new HashSet<String>(); for (int i = 0; i < txt.length(); i++) { int length = CheckSensitiveWord(txt, i, matchType); if (length > 0) { sensitiveWordList.add(txt.substring(i, i + length)); i = i + length - 1; } } return sensitiveWordList; } public String replaceSensitiveWord(String txt, int matchType, String replaceChar) { String resultTxt = txt; Set<String> set = getSensitiveWord(txt, matchType); Iterator<String> iterator = set.iterator(); String word = null; String replaceString = null; while (iterator.hasNext()) { word = iterator.next(); replaceString = getReplaceChars(replaceChar, word.length()); resultTxt = resultTxt.replaceAll(word, replaceString); } return resultTxt; }
private String getReplaceChars(String replaceChar, int length) { String resultReplace = replaceChar; for (int i = 1; i < length; i++) { resultReplace += replaceChar; } return resultReplace; }
public int CheckSensitiveWord(String txt, int beginIndex, int matchType) { boolean flag = false; int matchFlag = 0; Map nowMap = sensitiveWordMap; for (int i = beginIndex; i < txt.length(); i++) { char word = txt.charAt(i); nowMap = (Map) nowMap.get(word); if (nowMap != null) { matchFlag++; if ("1".equals(nowMap.get("isEnd"))) { flag = true; if (SensitiveFilter.minMatchType == matchType) { break; } } } else { break; } } if (SensitiveFilter.maxMatchType == matchType){ if(matchFlag < 2 || !flag){ matchFlag = 0; } } if (SensitiveFilter.minMatchType == matchType){ if(matchFlag < 2 && !flag){ matchFlag = 0; } } return matchFlag; } }
|
1.4、控制层
1 2 3 4 5 6 7
| SensitiveFilter filter = SensitiveFilter.getInstance(); int n = filter.CheckSensitiveWord(searchkey,0,1); if(n > 0){ logger.info("这个人输入了非法字符--> {},不知道他到底要查什么~ userid--> {}",searchkey,userid); return null; }
|
也可将敏感文字替换\*
等字符 :
1 2 3
| SensitiveFilter filter = SensitiveFilter.getInstance(); String text = "敏感文字"; String x = filter.replaceSensitiveWord(text, 1, "*");
|
2、订单系统
本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考。
1.1、订单系统在企业中的角色
在搭建企业订单系统之前,需要先梳理企业整体业务系统之间的关系和订单系统上下游关系,只有划分清业务系统边界,才能确定订单系统的职责与功能,进而保证各系统之间高效简洁的工作。
1.2、 订单系统与各业务系统的关系
(1)对外系统:
所有给企业外部用户使用的系统都在这一层,包括官网、普通用户使用的C端,还包括给商户使用的商家后台和在各个销售渠道进行分销的系统,比如与银行信用卡中心合作、微信合作在合作商的平台露出本企业的产品。这类系统站在与客户接触的最前线,是公司实现商业模式的桥头堡。
(2)管理中后台:
每个C端的业务形态都会有一个对应的系统模块,如负责管理平台交易的订单系统,管理优惠信息的促销系统,管理平台所有产品的产品系统,以及管理所有对外系统显示内容的内容系统等。
(3)公共服务系统:
随着企业的发展,信息化建设到达一定程度后,企业需要将通用功能服务化、平台化,以保证应用架构的合理性,提升服务效率。这类系统主要给其他应用系统提供基础服务能力支持。
1.3、 订单系统上下游关系
(1)订单服务
该模块的主要功能是用户日常使用的服务和页面,主要有订单列表、订单详情、在线下单等,还包括为公共业务模块提供的多维度订单数据服务。
(2)订单逻辑
订单系统的核心,起着至关重要的作用,在订单系统负责管理订单创建、订单支付、订单生产、订单确认、订单完成、取消订单等订单流程。还涉及到复杂的订单状态规则、订单金额计算规则以及增减库存规则等。在4节核心功能设计中会重点来说。
(3)底层服务
信息化建设达到一定程度的企业,一般会将公司公共服务模块化,比如:产品,会构建对应的产品系统,代码、数据库,接口等相对独立。但是,这也带来了一个问题,比如:订单创建的场景下需要获取的信息分散在各个系统。
如果需要从各个公共服务系统调用:一是会花费大量时间,二是代码的维护成本非常高。因此,订单系统接入所需的公共服务模块接口,在订单系统即可完成对接公共系统的服务。
1.4、订单系统核心功能
1.4.1、内容
为了使订单系统能够对订单进行高效、精准的管理和跟踪,订单会储存关于产品、优惠、用户、支付信息等一系列的订单实时数据,来和下游系统,如:促销、仓储、物流进行交互。
以一个通用B2C商城的订单为例,梳理其包含的信息如下:
这里要注意的是订单类型,随着平台业务的不断发展,品类丰富、交易方式丰富后,需要对订单进行多维度的分类管理,同时订单类型利于订单系统的扩展性。每种订单类型将会对应一套流程及一套状态,便于对订单进行分类管理和复用。
1.4.2、流程
流程是指从平台角度出发,将订单从创建到完成的整个流转过程进行抽象,从而形成了一套标准流程规则。而不同的产品类型或交易类型在系统中的流程会千差万别,因此为了方便对订单流程进行管理,会组建流程引擎模块。
每套订单流程中会包含正向流程及逆向流程,正向流程可以比作一次顺利的网购体验过程中,后台系统之间的信息流转。逆向流程则是修改订单、取消订单、退款、退货等各种动作引起的后台系统流程,同时每个流程触发的条件又可分为系统触发和人工触发两种场景。
(1)正向流程
以一个通用B2C商城的订单系统为例,根据其实际业务场景,其订单流程可抽象为5大步骤:订单创建>订单支付>订单生产>订单确认>订单完成。
而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图:
订单创建:
用户下单后,系统需要生成订单,此时需要先获取下单中涉及的商品信息,然后获取该商品所涉及到的优惠信息,如果商品不参与优惠信息,则无此环节。
接着获取该账户的会员权益,这里要注意的是:优惠信息与会员权益的区别,比如:商品满减是优惠信息,SUPER会员全场9.8折指的是会员权益,一个是针对商品,另一个是针对账户。其次就是优惠活动的叠加规则和优先级规则等。
增减库存规则是指订单中的商品,何时从仓储系统中对相应商品库存进行扣除,目前主流有两种方式:
下单减库存——即用户下单成功时减少库存数量
- 优势:用户体验友好,系统逻辑简洁;
- 缺点:会导致恶意下单或下单后却不买,使得真正有需求的用户无法购买,影响真实销量;
解决办法:
- 设置订单有效时间,若订单创建成功N分钟不付款,则订单取消,库存回滚;
- 限购,用各种条件来限制买家的购买件数,比如一个账号、一个ip,只能买一件;
- 风控,从技术角度进行判断,屏蔽恶意账号,禁止恶意账号购买。
付款减库存——即用户支付完成并反馈给平台后再减少库存数量
- 优势:减少无效订单带来的资源损耗;
- 缺点:因第三方支付返回结果存在时差,同一时间多个用户同时付款成功,会导致下单数目超过库存,商家库存不足容易引发断货和投诉,成本增加。
解决办法:
- 付款前再次校验库存,如确认订单要付款时再验证一次,并友好提示用户库存不足;
- 增加提示信息:在商品详情页,订单步骤页面提示不及时付款,不能保证有库存等。
综上所述,两种方式各有优缺点,因此,需结合实际场景进行考虑,如:秒杀、抢购、促销活动等,可使用下单减库存的方式。而对于产品库存量大,并发流量没有那么强的产品使用付款减库存的方式。
将两种方式带入到销售场景中,关联商品类型、促销类型、供需关系等,灵活使用,以充分发挥计算机系统的优势。
订单支付:
用户支付完订单后,需要获取订单的支付信息,包括支付流水号、支付时间等。支付完订单接着就是等商家发货,但在发货过程中,根据平台业务模式的不同,可能会涉及到订单的拆分。
订单拆分一般分两种:
- 一种是用户挑选的商品来自于不同渠道(自营与商家,商家与商家);
- 另一种是在SKU层面上拆分订单:不同仓库,不同运输要求的SKU,包裹重量体积限制等因素需要将订单拆分。
订单拆分也是一个相对独立的模块,这里就不详细描述了。
订单生产:订单生产,是指产品从企业到用户这一流程的概述。如电商平台中,商家发货过程已有一个标准化的流程,订单内容会发送到仓库,仓库对商品进行打单、拣货、包装、交接快递进行配送。
订单确认:收到货后,订单系统需要在快递被签收后提醒用户对商品做评价。这里要注意,确认收到货不代表交易成功,相反是售后服务的开始。
订单完成:订单完成是指在收到货X天的状态,此时订单不在售后的支持时间范围内。到此,一个订单的正向流程就算走完了。
(2)逆向流程
上面说到逆向流程是各种修改订单、取消订单、退款、退货等操作,需要梳理清楚这些流程与正向流程的关系,才能理清订单系统完整的订单流程。
订单修改:可梳理订单内信息,根据信息关联程度及业务诉求,设定订单的可修改范围是什么,比如:客户下单后,想修改收货人地址及电话。此时只需对相应数据进行更新即可。
订单取消:用户提交订单后没有进行支付操作,此时用户原则上属于取消订单,因为还未付款,则比较简单,只需要将原本提交订单时扣减的库存补回,促销优惠中使用的优惠券,权益等视平台规则,进行相应补回。
退款:用户支付成功后,客户发出退款的诉求后,需商户进行退款审核,双方达成一致后,系统应以退款单的形式完成退款,关联原订单数据。因商品无变化,所以不需考虑与库存系统的交互,仅需考虑促销系统及支付系统交互即可。
退货:用户支付成功后,客户发出退货的诉求后,需商户进行退款审核,双方达成一致后,需对库存系统进行补回,支付系统、促销系统以退款单形式完成退款。最后,在退款/退货流程中,需结合平台业务场景,考虑优惠分摊的逻辑,在发生退款/退货时,优惠该如何退回的处理规则和流程。
(3)状态机
状态机是管理订单状态逻辑的工具。状态机可归纳为3个要素,即现态、动作、次态。
- 现态:是指当前所处的状态。
- 动作:动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。
- 次态:动作满足后要迁往的新状态,“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
状态机的设计需要结合平台实际业务场景,将状态间的切换细化成了执行了某个动作。
以一个B2C商城的订单系统举例如下:
订单系统为了高效的对订单进行跟踪和管理,会对订单流程当中的关键节点,抽象出订单状态。而订单状态从不同用户的角度可分为,系统订单状态、商家订单状态、买家订单状态等。
对于订单系统来说,订单状态细分的颗粒度越细、越明确,订单系统管理的精度和可靠性就越高,比如:在待付款和待发货两个状态中,订单系统后台会细分为订单超时取消、订单支付失败、订单付款完成等。
因此,订单状态模块中,通常会维护状态映射表,以不同的用户角色对系统订单状态进行重新划分,以满足不同用户的需求。
除此以外,随着电商平台的不断发展,不同的业务类型,所对应的订单状态都会有所区别。所以,订单系统中一般会储存多套状态机,以满足不同的订单类型来使用。
3、SpringBoot+Redis 接口限流
3.1、搭建环境
添加依赖:
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.1.4.RELEASE</version> </dependency>
|
3.2、自定义限流注解
1 2 3 4 5 6
| @Retention(RUNTIME) @Target(METHOD) public @interface AccessLimit { int seconds(); int maxCount(); }
|
3.3、自定义RedisService接口
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
| public interface RedisService {
boolean set(String key, String value);
String get(String key);
boolean expire(String key, long expire);
boolean remove(String key);
Long incr(String key,long time); }
|
3.4、RedisService 接口的实现类
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
| redisservice的实现类
@Service("redisService") public class RedisServiceImpl implements RedisService { @Resource private RedisTemplate<String, ?> redisTemplate; @Override public boolean set(final String key, final String value) { boolean result = redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = redisTemplate.getStringSerializer(); connection.set(serializer.serialize(key), serializer.serialize(value)); return true; } }); return result; } @Override public String get(final String key) { String result = redisTemplate.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = redisTemplate.getStringSerializer(); byte[] value = connection.get(serializer.serialize(key)); return serializer.deserialize(value); } }); return result; } @Override public boolean expire(final String key, long expire) { return redisTemplate.expire(key, expire, TimeUnit.SECONDS); } @Override public boolean remove(final String key) { boolean result = redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisSerializer<String> serializer = redisTemplate.getStringSerializer(); connection.del(key.getBytes()); return true; } }); return result; } @Override public Long incr(String key,long time){ long count = redisTemplate.opsForValue().increment(key, 1); if (count == 1) { set(key,"1"); redisTemplate.expire(key, time, TimeUnit.SECONDS); } return count; } }
|
3.5、自定义拦截器
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
| @Configuration public class MyInterceptor implements HandlerInterceptor{ @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if (accessLimit != null) { long seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount();
String key = SystemUtil.getClientIp(httpServletRequest)+hm.getMethod().getName(); try { long q = redisService.incr(key, seconds); if (q > maxCount) { render(httpServletResponse, new ResponseMsg(0, "请求过于频繁,请稍候再试", null)); return false; } return true; }catch (RedisConnectionFailureException e){ logger.info("redis错误"+e.getMessage().toString()); return true; } } } return false; } private void render(HttpServletResponse response, ResponseMsg cm) throws Exception { response.setContentType("application/json;charset=UTF-8"); OutputStream out = response.getOutputStream(); String str = new Gson().toJson(cm); out.write(str.getBytes("UTF-8")); out.flush(); out.close(); } }
|
3.6、使用限流的注解
在需要限流的Controller方法中,加上自定义的限流注解即可:
1 2 3 4 5 6 7 8
| public class HelloController{ @AccessLimit(seconds=second, maxCount=maxCount) @GetMapping("/hello") public MyRst hello(){ return MyRst("请求成功!"); } }
|