文章目录
软件开发整体介绍
设计流程
- 需求分析,需求规格说明书、产品原型
- 设计,UI设计、数据库设计、接口设计
- 编码,项目代码、单元测试
- 测试,测试用例、测试报告
- 上线运维,软件环境安装、配置
分工
项目经理:对整个项目负责,任务分配、把控进度
产品经理:进行需求调研,输出需求调研文档、产品原型等
UI设计师:根据产品原型输出界面效果图
架构师:项目整体架构设计、技术选型等
开发工程师:代码实现
测试工程师:编写测试用例,输出测试报告
运维工程师:软件环境搭建、项目上线
苍穹外卖项目介绍
分为前端(管理端和用户端),后端(java)
管理端(Web)
- 员工管理
- 分类管理
- 菜品管理
- 套餐管理
- 订单管理
- 工作台
- 数据统计
- 来电提醒
用户端(微信小程序) - 微信登录
- 商品浏览
- 购物车
- 用户下单
- 微信支付
- 历史订单
- 地址管理
- 用户催单
后端环境搭架
父项目中的子模块
- sky-common,公共类,供其他模块使用
- sky-pojo,ebtity\DTO\VO
- sky-server,拦截器\controller\service\mapper\启动类等
git版本控制
- 创建git本地仓库
- 创建git远程仓库
- 将本地文件推送到git远程仓库中(gitee)
.gitignore是git提交忽略配置文件
数据库搭建
前后端联调
为什么前端请求地址和后端访问地址不相同,还可以访问到,为什么要这么做(反向代理)?
nginx反向代理(好处)
- 提高访问速度(后端数据可以在nginx上缓存)
- 进行负载均衡
- 保证后端服务安全
登录功能完善-MD5加密
123456->dhaskdbcaskcuywia(MD5算法不可逆)
// md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
接口文档
yapi导入json接口文档
swagger
介绍和使用方式
Knife4j,为javaMVC框架集成swagger生成Api文档的增强解决方案
- swagger,扫描controller类,也就是根据在后端已经实现的接口生成接口文档
- 可通过’http://localhost:8080/doc.html#/home’类似地址查看当前实现接口,
- 还能做接口测试,一定程度上可替代postman
和Yapi不冲突,设计阶段工具,管理维护接口,而swagger是开发阶段使用的框架,帮助开发人员做后端的接口测试
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
}
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket() {
log.info("准备生成接口文档");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
常用注解
让swagger生成的Api文档拥有更好的可读性
- @Api,用在Controller等类上
- @ApiModel,用在entity\DTO\VO等类上
- @ApiModelProperty,用在属性上,描述属性信息
- @ApiOperation,用在方法上,说明方法的用途作用
新增员工
需求分析
项目约定
- 管理端发出的请求,统一用/admin作为前缀
- 用户端发出的请求,统一用/user作为前缀
功能完善
- 重复新增全局异常捕获
- 记录创建和修改人id
同一次请求在服务端同属一个线程
分页查询
代码开发
PageHelper的底层原理也是通过localthread线程的原理将页码和页面大小从service层传到mapper层
复杂的查询需求写在映射文件xml中
alt+enter\alt+insert两个常用的代码生成快捷键
功能完善
- 对返回给前端的日期格式化
/**
* 消息转换器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入到容器中,0代表优先使用,转换器排在第一个使用
converters.add(0,converter);
启用禁用员工账号
Employee类上有@Builder注解,可以通过以下方式实体类构造
//实体类构造器
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
编辑员工信息
根据id回显员工信息
编辑员工信息
接口调试时返回为空可能是token令牌过期
分类管理模块(菜品和套餐分类)
具体操作与员工管理的代码较为相似,不再赘述
新增模块(复制粘贴过来的新文件,手写的是会自动编译的)记得编译compile
公共字段自动填充
枚举(enum)是一种特殊的类
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
自定义注解用于加在mapper中的方法上
@Target(ElementType.METHOD)//指定此注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型,UPDATE,INSERT
OperationType value();
}
使用AOP机制,为创建人\创建时间\修改人\修改时间设定值
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值
* @param joinPoint
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始公共字段自动填充");
//获取到当前被拦截的方法上的数据库操作类型
//方法签名对象
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取方法上的注解对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
//获取数据库操作类型
OperationType operationType = autoFill.value();
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型(update和insert),为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try{
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER,Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
}
catch(Exception e){
e.printStackTrace();
}
}
else if(operationType == OperationType.UPDATE){
//为4个公共字段赋值
try{
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
}
catch(Exception e){
e.printStackTrace();
}
}
}
}
最后需要在特定方法上添加注解,使之添加对应的通知处理
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
新增菜品
- 这里要实现向菜品表中插入1条数据,向口味表中插入n条数据
- 注意如何获取菜品表中的主ID插入到口味表中
菜品分页查询
- 还是要注意pagehelper如何使用
- 注意要如何从分类表中获取分类名称.SQL语句要怎么写
删除菜品
- 共要操作三张表,菜品表,口味表,菜品和套餐的中间关系表
修改菜品
- 注意不仅要修改菜品表,还要修改口味表
Redis
Redis是一个基于内存的key-value结构数据库
官网,www.redis.net.cn
- 基于内存存储,读写性能高
- 适合存储热点数据(热点商品、资讯、新闻)
- 企业应用广泛
redis属于绿色软件(文件夹),解压即可使用
启动redis服务
cmd中启动redis-server.exe
客户端(这里的客户指的是开发人员)使用服务
cmd中启动redis-cli.exe
命令示例,redis-cli.exe -h localhost -p 6379 -a 123456
-h 地址 -p 端口号 -a 密码
密码需要在redis.windows-service.conf手动设置
虽然可以在命令行使用redis,但一般还是图形界面的redis更好用更主流(还是要手动启动redis服务的)
常用数据类型
key为字符串类型,value有5种常用的数据类型
- 字符串string
- 哈希hash ,类似HashMap
- 列表list ,类似LinkedList
- 集合set ,类似HashSet
- 有序集合sorted set/zset 集合中每个元素关联一个分数score,根据分数升序
字符串操作命令
- SET key value ,设置指定key的值
- GET key ,获取指定key的值
- SETEX key seconds value ,设置指定key值,并将key的过期时间设置为sencond秒
- SETNX key value ,只有在key不存在时,设置key的值
哈希操作命令
- HSET key field value,设置值
- HGET key field ,获取值
- HDEL key field,删除值
- HKEYS key,获取表中所有字段
- HVALS key,获取表中所有值
列表操作命令
- LPUSH key value1 [value],将一个或多个值插入头部(后插入的那端为头部)
- LRANGE key start stop,获取列表指定范围的元素
- RPOP key,移除并获取列表最后一个元素,也就是第一个被插入的
- LLEN KEY,获取列表长度
集合操作命令
- SADD key mamber1 [member2],向集合添加一个或多个成员
- SMEMBER key,返回集合中的所有成员
- SCARD key,获取集合的成员数
- SINTER key1 [key2],返回给定集合的交集
- SUNION key1 [key2],返回给定集合的并集
- SREM key member1 [member2],删除集合中的一个或多个成员
有序集合操作命令
每个元素关联一个double类型的分数
- ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
- ZRANGE key start stop [WITHSCORS],通过索引区间返回指定区间的成员
- ZINCRBY key increment member,添加上增量increment
- ZREM key member [member…],移除集合中的一个或多个成员
通用命令
- KEYS pattern ,查找所有符合给定模式的key
- EXISTS key, 检查给定key是否存在
- TYPE key , 返回key所存储的值的类型
- DEL key ,该命令用于在key存在时,删除key
在java中操作redis,Spring Data Redis
使用方法
- 导入spring data redis 的maven坐标
- 配置redis数据源
- 编写配置类,创建redistemplate对象
- 通过redistemplate对象操作redis
maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis数据源,application.yml文件
redis:
host: localhost
port: 6379
password: 123456
database: 10
编写配置类
@Configuration
@Slf4j
public class RedisConfiguration {
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("redisTemplate,开始创建redis模板对象...");
RedisTemplate redisTemplate =new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
操作字符串数据
@Autowired
private RedisTemplate redisTemplate;
public void testString(){
//set get setex setnx
redisTemplate.opsForValue().set("name","xiaoming");
System.out.println((String) redisTemplate.opsForValue().get("name"));
redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);//setex
redisTemplate.opsForValue().setIfAbsent("lock","1");//setnx
}
操作哈希数据
public void testHash(){
//hset hget hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100","name","tom");
hashOperations.put("100","age","22");
System.out.println((String) hashOperations.get("100","name"));
Set keys = hashOperations.keys("100");
System.out.println(keys);
List values = hashOperations.values("100");
System.out.println(values);
hashOperations.delete("100","name");
}
操作列表数据
@Test
public void testList(){
//lpush lrange rpop llen
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPushAll("mylist","a","b","c");
listOperations.leftPush("mylist","d");
List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist");
System.out.println(size);
}
操作集合数据
public void testSet(){
//sadd smember scard sinter sunion srem
SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("myset1","a","b","c");
setOperations.add("myset2","b","c","d");
setOperations.members("myset1");
setOperations.size("myset1");
setOperations.intersect("myset1","myset2");
setOperations.union("myset1","myset2");
setOperations.remove("myset1","a");
}
操作有序集合数据
public void testZset(){
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","a",10);
zSetOperations.add("zset2","b",20);
zSetOperations.add("zset3","c",30);
zSetOperations.range("zset1",0,-1);
zSetOperations.incrementScore("zset1","c",10);
zSetOperations.remove("zset1","c");
}
通用操作命令
public void testCommon(){
//keys exists type del
Set keys = redisTemplate.keys("*");
redisTemplate.hasKey("name");
for(Object key : keys){
redisTemplate.type(key);
}
redisTemplate.delete("name");
}
店铺营业状态设置
代码很简单,主要是应用redis
HttpClient
可以用来提供高效的\最新的\功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议
maven坐标
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
发送GET方式请求
xxx.var是创建对象的快捷方式
/**
* 通过httpclient发送GET请求
*/
@Test
public void testGet() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求
CloseableHttpResponse response = httpClient.execute(httpGet);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务器返回状态码"+statusCode);
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据"+body);
//关闭资源
response.close();
httpClient.close();
}
发送post方式请求
/**
* post方式
* @throws IOException
*/
@Test
public void testPOST() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", "admin");
jsonObject.put("password", "123456");
StringEntity entity = new StringEntity(jsonObject.toString());
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
CloseableHttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为"+statusCode);
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为"+body);
}
微信小程序
小程序ID wxa431188d22e68c6f
小程序密钥 7fb396a34c8de8b60626810145d85b89
小程序的目录结构
页面由四个文件组成,js(页面逻辑),wxml(页面结构),json(页面配置),wxss(页面样式)
logincode 0e1PyoGa1Xk6AI0UsOFa1Jwbcv1PyoGF
微信登录
@Configuration和@Bean联用,将第三方写好的类注入到spring框架中
@Component是注入自定义的类对象
缓存
为了用户能更高效地访问数据,需要使用redis将一些重要的反复需要访问的数据缓存
对服务器的压力也大
redis缓存只是将数据缓存在服务端中,客户端还是需要保持连接访问,或者使用其他技术将数据缓存在本地
菜品缓存
缓存
- 每个分类下的菜品保存一份缓存数据
- 数据库中菜品数据有变更时清理缓存数据
就一个原则,怎么存进去的就怎么强转取出来就行了
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构造redis中的key,规则:dish_分类id
String key = "dish_"+categoryId;
//查询redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
//如果存在,直接返回,无须查询数据库
if(list !=null && list.size()>0){
return Result.success(list);
}
//如果不存在,查询数据库,将查询到的数据放入redis中
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
清理缓存
当修改了被访问的数据,需要清理缓存,否则用户访问的是修改前的数据
- 新增菜品
- 修改菜品
- 批量删除菜品
- 起售\停售菜品
这里实现的就是简单粗暴的都清理掉
/**
* 清理缓存数据
* @param pattern
*/
public void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
缓存套餐
spring cache框架,通过注解的方式缓存数据
maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- @EnableCacheing,开启缓存注解功能,通常加载启动类上
- Cacheable,方法执行前先查询是否有数据,有数据返回缓存数据,没有,调用犯法放入缓存,一般就是将这个函数的返回值放入或缓存和调出缓存
- CachePut,将方法的返回值放到缓存中
- CacheEvict,将一条或多条数据从缓存中删除
因为是根据分类查询的菜品和套餐,所以这里缓存清除和缓存的逻辑还要好好琢磨一下,为什么是删除一个套餐时,清除的是整个分类,而不是单个套餐去在缓存中清理
添加购物车
在设计表与表时,可存在冗余字段,可提高查询速度,就不用查多个表了,省时间就要费空间
查看购物车
清空购物车
导入地址簿
- 查询地址列表
- 新增地址
- 修改地址
- 删除地址
- 设置默认地址
- 查询默认地址(在下单时,需要展示)
用户下单
订单支付
需要企业认证,个人无法认证微信支付
- 付款码支付
- JSAPI支付
- 小程序支付
- Native支付
- APP支付
- 刷脸支付
这里用传智教育的商户号
微信支付准备工作
- 准备证书文件
- 为当前电脑生成临时ip
./cpolar authtoken MzY1ZGI1OTAtNTgzYy00MjdmLWFlYTItYTBjMTQ1ZjI0NWNm
//开启cpolar服务
$ ./cpolar http 80
//关联端口号
cpolar,通过内网穿透,临时获取一个内网域名,可以访问当前电脑上的资源,因为我连接了互联网,所以在我的手机上也能访问电脑资源
订单相关模块
用户端订单管理
查询历史订单
查询订单详情
取消订单
再来一单
商家端订单管理模块
订单搜索
各状态订单数量统计
查询订单详情
接单
拒单
取消订单
派送订单
完成订单
校验收货地址是否超出配送范围
AK:GaFQYZYzEJW6ky7sm62xBxHxpBNRyc1D
address:辽宁省沈阳市和平区文化路3号巷11号东北大学(南湖校区)
定时处理
Spring Task
提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑
比如,信用卡每月还款提醒\火车票售票系统处理未支付订单
cron表达式,规则不用记,预期的定时可以用在线的cron生成器生成
不用导入maven坐标,springboot集成了
- 启动类添加注解,@EnableScheduling //开启任务调度
- 自定义定时任务类
订单定时状态处理
- 下单后未支付,订单一直处于待支付状态
解决方案,通过定时任务每分钟检查一次是否存在支付超时订单,存在就取消
相比于针对每个用户的定时开销小太多了,也没必要定时1s就查一次,这对服务器的压力也很大 - 用户收货后管理端未点击完成按钮,订单一直处于派送中状态
解决方案,每天凌晨(或者打烊后)检查一次是否存在派送中订单
WebSocket
是基于TCP的一种新的网络协议,实现了浏览器与服务器全双工通信-浏览器与服务器只需要一次握手,就可创建持久性的连接,并双向数据传输
HTTP协议只能先由客户端发出请求再由服务端响应
- 短链接
- 单向
而webSocket在握手后就可以双向传输,很流氓,我客户端没要,你凭什么提供给我,但确实使客户端傻瓜操作 - 长连接
- 双向
- 应用,视频弹幕\网页聊天\股市体育实时更新
maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
但底层都基于TCP协议
*Configuration.java
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
*websocketserver.java
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
可以使用定时任务周期骚扰
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
来单提醒及客户催单
springTask和webSocket的联用
和HTTP协议的接口文档规定一样,websocket同样需要前后端的约定
约定通讯的数据格式为json,type字段为数据类型
来单提醒
实现不了,需要在实现支付后微信官方回调我本地服务器的Controller
客户催单
统计功能模块
Apache ECharts
基于javascript的数据可视化图表库(前端技术)
作为后端程序员,使用echarts,重点在于研究当前图表所需的数据格式.
营业额统计
用户统计
订单统计
销量统计
工作台模块
导出运营数据excel报表
Apache POI
处理miscrosoft Office各种文件的开源项目,可以使用POI在java程序中对office各种文件进行读写操作
maven坐标
<!-- poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
导出运营数据excel报表
添加"运营数据报表模板.xlsx"模板文件到后端resources/template中