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

外卖项目技术亮点总结笔记

最近跟着听了苍穹外卖项目,想要梳理一下基础增删改查以外的一些技术亮点,方便融入简历和面试被询问

自动填充功能

1. 基于消息转化器实现自动更改或创建日期

    /*** 扩展Spring MVC框架的消息转化器* @param converters*/protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {log.info("扩展消息转换器...");//创建一个消息转换器对象MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据converter.setObjectMapper(new JacksonObjectMapper());//将自己的消息转化器加入容器中converters.add(0,converter);}

首先在WebMvcConfiguration里定义一个扩展消息转化器的方法,首先看一下什么是消息转换器
核心功能
请求数据转换
将HTTP请求中的原始数据(如JSON、XML)转换为Controller方法参数所需的Java对象。例如,将前端传来的JSON字符串自动映射为@RequestBody注解修饰的POJO对象。

响应数据转换
将Controller方法返回的Java对象转换为HTTP响应所需的格式(如JSON、XML)。例如,通过@ResponseBody将返回的List序列化为JSON字符串。

我们定义一个消息转化器专门转换日期格式,全局统一处理,不需要在实体类上添加JsonFormat注解

我们需要一个类 定义好转换格式和序列化时的处理过程

/*** 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]*/
public class JacksonObjectMapper extends ObjectMapper {public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";//public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";public JacksonObjectMapper() {super();//收到未知属性时不报异常this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);//反序列化时,属性不存在的兼容处理this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);SimpleModule simpleModule = new SimpleModule().addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))).addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))).addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))).addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))).addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))).addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));//注册功能模块 例如,可以添加自定义序列化器和反序列化器this.registerModule(simpleModule);}
}

这样在前后端数据交互序列化的时候,就会自动处理好日期格式。

2. 通过AOP拦截和自定义注解实现字段填充

我们在增 和 改的时候 有一些公共字段需要填充,比如创建时间和创建人id以及修改时间和修改人id,但是在方法里写冗余代码太多,每添加一个增改方法就要写一遍,写成一个公共方法仍然得调用,不如添加注解省事清晰。

AOP的概念
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过分离横切关注点(如日志、事务、安全等)来提高代码的模块化。AOP允许将通用功能从业务逻辑中剥离,通过动态代理或字节码操作等技术在运行时将切面代码织入目标方法中。

AOP的核心组成
切面(Aspect):封装横切关注点的模块,通常包含通知和切点。
通知(Advice):定义在切点处执行的逻辑,分为前置(Before)、后置(After)、环绕(Around)等类型。
切点(Pointcut):通过表达式匹配需要注入切面的方法或类。
连接点(Join Point):程序执行过程中可插入切面的点,如方法调用或异常抛出。

先定义一个注解

/*** 自定义注解,用于标识某个方法需要进行功能字段自动填充处理*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {//数据库操作类型:UPDATE INSERTOperationType value();
}

表明该注解是用在方法上的,运行时生效,有一个值,我们看一下这个值

/*** 数据库操作类型*/
public enum OperationType {/*** 更新操作*/UPDATE,/*** 插入操作*/INSERT}

就是在用这个注解上要用这个枚举类里定义好的值,是插入还是更新
然后定义一个切片方法

/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@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();//根据当前不同的操作类型,为对应的属性通过反射来赋值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){//为2个公共字段赋值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();}}}}

首先通过切入点表达式找到切入点,也就是满足 mapper包下的所有类,所有方法,所有参数 以及 有这个AutoFill注解 的方法。

在添加注解的方法执行前执行前置通知方法,首先通过连接点反射获取注解里的值,看是插入还是更新

获取当前的时间和当前用户ID (通过BaseContext.getCurrentId(); 底层是基于ThreadLocal 存储用户id,可以避免多线程竞争冲突)

通过反射给属性赋值,属性名定义了一个常量类

/*** 公共字段自动填充相关常量*/
public class AutoFillConstant {/*** 实体类中的方法名称*/public static final String SET_CREATE_TIME = "setCreateTime";public static final String SET_UPDATE_TIME = "setUpdateTime";public static final String SET_CREATE_USER = "setCreateUser";public static final String SET_UPDATE_USER = "setUpdateUser";
}

微信登录功能

wx.login获取授权码, 将授权码返回后端,后端拿appid和秘钥 还有 用户授权码请求微信接口服务,
微信接口服务返回session key和open id,后端用token记录下来返回前端,用户请求都带上这个token

/*** 微信登录* @param userLoginDTO* @return*/@PostMapping("/login")@ApiOperation("微信登录")public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){log.info("微信用户登录:{}",userLoginDTO.getCode());//微信登录User user = userService.wxLogin(userLoginDTO);//为微信用户生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put(JwtClaimsConstant.USER_ID,user.getId());String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);UserLoginVO userLoginVO = UserLoginVO.builder().id(user.getId()).openid(user.getOpenid()).token(token).build();return Result.success(userLoginVO);}

核心就是Open ID这个是用户唯一标识,根据这个去看是否新用户,不是就注册

    /*** 微信登录* @param userLoginDTO* @return*/public User wxLogin(UserLoginDTO userLoginDTO) {String openid = getOpenid(userLoginDTO.getCode());//判断openid是否为空,如果为空表示登录失败,抛出业务异常if(openid == null){throw new LoginFailedException(MessageConstant.LOGIN_FAILED);}//判断当前用户是否为新用户User user = userMapper.getByOpenid(openid);//如果是新用户,自动完成注册if(user == null){user = User.builder().openid(openid).createTime(LocalDateTime.now()).build();userMapper.insert(user);}//返回这个用户对象return user;}

微信支付功能

先看一下微信支付的流程图
在这里插入图片描述
用户下单就根据用户id 地址电话id 菜品id 订单号等创建一个订单信息,状态设置为未支付。

确认支付后先根据 订单号 金额 微信openid 请求jsapi接口,返回解析后返回小程序端 时间戳,签名随机字符串,预支付id,签名方式 和 支付签名
先看一下VO返回前端数据

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderPaymentVO implements Serializable {private String nonceStr; //随机字符串private String paySign; //签名private String timeStamp; //时间戳private String signType; //签名算法private String packageStr; //统一下单接口返回的 prepay_id 参数值
}

在看一下组装过程

    /*** 订单支付** @param ordersPaymentDTO* @return*/public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {// 当前登录用户idLong userId = BaseContext.getCurrentId();User user = userMapper.getById(userId);//调用微信支付接口,生成预支付交易单JSONObject jsonObject = weChatPayUtil.pay(ordersPaymentDTO.getOrderNumber(), //商户订单号new BigDecimal(0.01), //支付金额,单位 元"苍穹外卖订单", //商品描述user.getOpenid() //微信用户的openid);if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {throw new OrderBusinessException("该订单已支付");}OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);vo.setPackageStr(jsonObject.getString("package"));return vo;}

根据传来的参数调用支付工具类发起支付请求,然后组装VO返回结果

 /*** 小程序支付** @param orderNum    商户订单号* @param total       金额,单位 元* @param description 商品描述* @param openid      微信用户的openid* @return*/public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {//统一下单,生成预支付交易单String bodyAsString = jsapi(orderNum, total, description, openid);//解析返回结果JSONObject jsonObject = JSON.parseObject(bodyAsString);System.out.println(jsonObject);String prepayId = jsonObject.getString("prepay_id");if (prepayId != null) {String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);String nonceStr = RandomStringUtils.randomNumeric(32);ArrayList<Object> list = new ArrayList<>();list.add(weChatProperties.getAppid());list.add(timeStamp);list.add(nonceStr);list.add("prepay_id=" + prepayId);//二次签名,调起支付需要重新签名StringBuilder stringBuilder = new StringBuilder();for (Object o : list) {stringBuilder.append(o).append("\n");}String signMessage = stringBuilder.toString();byte[] message = signMessage.getBytes();Signature signature = Signature.getInstance("SHA256withRSA");signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))));signature.update(message);String packageSign = Base64.getEncoder().encodeToString(signature.sign());//构造数据给微信小程序,用于调起微信支付JSONObject jo = new JSONObject();jo.put("timeStamp", timeStamp);jo.put("nonceStr", nonceStr);jo.put("package", "prepay_id=" + prepayId);jo.put("signType", "RSA");jo.put("paySign", packageSign);return jo;}return jsonObject;}

拿到预支付id,然后给出时间戳,随机字符,加密方式,支付方式等数据返回,这些参数要传给前端,前端拿着加密请求微信后台,微信后台返回的时候用这样的方式解密,防止数据在网络传输中被人截胡。

   /*** jsapi下单** @param orderNum    商户订单号* @param total       总金额* @param description 商品描述* @param openid      微信用户的openid* @return*/private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception {JSONObject jsonObject = new JSONObject();jsonObject.put("appid", weChatProperties.getAppid());jsonObject.put("mchid", weChatProperties.getMchid());jsonObject.put("description", description);jsonObject.put("out_trade_no", orderNum);jsonObject.put("notify_url", weChatProperties.getNotifyUrl());JSONObject amount = new JSONObject();amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());amount.put("currency", "CNY");jsonObject.put("amount", amount);JSONObject payer = new JSONObject();payer.put("openid", openid);jsonObject.put("payer", payer);String body = jsonObject.toJSONString();return post(JSAPI, body);}

具体的请求预支付方法是以上代码,填好这些参数,appid,商户号,描述,订单号,回调函数的url,货币,金额,用户openid,用户id

    /*** 发送post方式请求** @param url* @param body* @return*/private String post(String url, String body) throws Exception {CloseableHttpClient httpClient = getClient();HttpPost httpPost = new HttpPost(url);httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());httpPost.setEntity(new StringEntity(body, "UTF-8"));CloseableHttpResponse response = httpClient.execute(httpPost);try {String bodyAsString = EntityUtils.toString(response.getEntity());return bodyAsString;} finally {httpClient.close();response.close();}}

构建一个post的httpclient请求,然后发送请求。

小程序端根据这些参数直接请求微信后台完成支付,小程序端显示支付成功,后台等待微信根据支付状态推送,接受到推送的密文后先解密,从解密后的json里取出 订单号(之前预支付时候提交的,自己后端的) 和 微信支付交易号,根据订单号修改订单支付状态。
我们看一下后台解密代码

/*** 支付成功回调** @param request*/@RequestMapping("/paySuccess")public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {//读取数据String body = readData(request);log.info("支付成功回调:{}", body);//数据解密String plainText = decryptData(body);log.info("解密后的文本:{}", plainText);JSONObject jsonObject = JSON.parseObject(plainText);String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号String transactionId = jsonObject.getString("transaction_id");//微信支付交易号log.info("商户平台订单号:{}", outTradeNo);log.info("微信支付交易号:{}", transactionId);//业务处理,修改订单状态、来单提醒orderService.paySuccess(outTradeNo);//给微信响应responseToWeixin(response);}/*** 读取数据** @param request* @return* @throws Exception*/private String readData(HttpServletRequest request) throws Exception {BufferedReader reader = request.getReader();StringBuilder result = new StringBuilder();String line = null;while ((line = reader.readLine()) != null) {if (result.length() > 0) {result.append("\n");}result.append(line);}return result.toString();}/*** 数据解密** @param body* @return* @throws Exception*/private String decryptData(String body) throws Exception {JSONObject resultObject = JSON.parseObject(body);JSONObject resource = resultObject.getJSONObject("resource");String ciphertext = resource.getString("ciphertext");String nonce = resource.getString("nonce");String associatedData = resource.getString("associated_data");AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));//密文解密String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);return plainText;}/*** 给微信响应* @param response*/private void responseToWeixin(HttpServletResponse response) throws Exception{response.setStatus(200);HashMap<Object, Object> map = new HashMap<>();map.put("code", "SUCCESS");map.put("message", "SUCCESS");response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));response.flushBuffer();}
}

微信支付的核心需要商获取,必须商家才可以,个人不可以
在这里插入图片描述
在这里插入图片描述

SpringCache缓存的使用

业务:访问主页菜品套餐查询缓存,提高访问效率,避免大量查询请求冲向数据库,只需要在增 改 删后删除掉缓存,重新查询时在添加回来。
Spring Cache 是 Spring 框架提供的一个 统一缓存抽象层,它屏蔽了底层缓存实现的差异(如 Caffeine、EhCache、Redis 等),让你通过简单注解就能实现方法级的缓存,大幅减少数据库或外部接口的重复调用。
引入方式

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

开启缓存

@SpringBootApplication
@EnableCaching   // 开启缓存功能
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

常用注解
@Cacheable(value = “usersCache”, key = “#id”)
作用:首次调用方法时执行并将结果放入缓存;之后相同参数调用直接返回缓存。
id为5的样子
key为 usersCache::5
values为 序列化对象(比如这个id对应的用户序列化对象)

适合放在查询方法上
@CachePut(value = “usersCache”, key = “#user.id”)
作用:方法每次都会执行,并将结果强制放入缓存(常用于更新数据时同步缓存)。
@CacheEvict(value = “usersCache”, key = “#id”)
清理缓存
@Caching(
put = { @CachePut(value = “usersCache”, key = “#user.id”) },
evict = { @CacheEvict(value = “userListCache”, allEntries = true) }
)
组合使用

SpringTask和WebSocket推送

业务: 主要是使用websocket推送订单 或 用户催单等消息,SpringTask定时清理未确认的订单 或 发送消息提醒之类的
WebSocket 是一种 基于 TCP 的全双工通信协议,它的主要特点是:

持久连接:不像 HTTP 一次请求一次响应,WebSocket 建立连接后,客户端和服务器都可以主动发送消息。

低延迟:适合实时推送消息(如订单提醒、聊天、股票行情)。

轻量化:握手时用 HTTP,建立后就走 WebSocket 协议,减少了重复的请求开销。
GET ws://localhost:8080/ws/123 HTTP/1.1
不是http开头 而是ws开头

配置一个bean和一个websocket服务类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** WebSocket配置类,用于注册WebSocket的Bean*/
@Configuration
public class WebSocketConfiguration {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;/*** WebSocket服务*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {//存放会话对象private static Map<String, Session> sessionMap = new HashMap();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {System.out.println("客户端:" + sid + "建立连接");sessionMap.put(sid, session);}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic 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();}}}}

spring task主要基于corn表达式
秒 分 时 日 月 星期 [年]
字段 允许值 特殊符号
秒 (Seconds) 0–59 , - * /
分 (Minutes) 0–59 , - * /
时 (Hours) 0–23 , - * /
日 (Day of month) 1–31 , - * ? / L W
月 (Month) 1–12 或 JAN–DEC , - * /
星期 (Day of week) 0–7 (0 或 7 = 周日) 或 SUN–SAT , - * ? / L #
年 (Year, 可选) 1970–2099 , - * /

* 代表所有值(任意)

? 代表不指定(日和星期冲突时用,二者不能同时指定)

, 多个值(如 1,2,3 表示第 1、2、3 秒)

- 范围(如 10-15 表示 10 到 15 秒)

/ 步长(如 0/5 表示从 0 开始每隔 5)

L 最后(如 L 表示当月最后一天,6L 表示当月最后一个星期五)

W 工作日(如 15W 表示距离 15 号最近的工作日)

# 指定第几个星期几(如 2#1 表示每月第一个周一)

 /*** 处理超时订单的方法*/@Scheduled(cron = "0 * * * * ? ") //每分钟触发一次public void processTimeoutOrder(){log.info("定时处理超时订单:{}", LocalDateTime.now());LocalDateTime time = LocalDateTime.now().plusMinutes(-15);// select * from orders where status = ? and order_time < (当前时间 - 15分钟)List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);if(ordersList != null && ordersList.size() > 0){for (Orders orders : ordersList) {orders.setStatus(Orders.CANCELLED);orders.setCancelReason("订单超时,自动取消");orders.setCancelTime(LocalDateTime.now());orderMapper.update(orders);}}}/*** 处理一直处于派送中状态的订单*/@Scheduled(cron = "0 0 1 * * ?") //每天凌晨1点触发一次public void processDeliveryOrder(){log.info("定时处理处于派送中的订单:{}",LocalDateTime.now());LocalDateTime time = LocalDateTime.now().plusMinutes(-60);List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);if(ordersList != null && ordersList.size() > 0){for (Orders orders : ordersList) {orders.setStatus(Orders.COMPLETED);orderMapper.update(orders);}}}

图表功能的使用

http://www.dtcms.com/a/443910.html

相关文章:

  • 做教育网站还挣钱吗dw做的网站怎么上传
  • 一诺网站建设湖南 微网站开发与设计比赛
  • 济南网站建设流程网站asp.net安装
  • 手机网站活动策划方案珠海品牌型网站建设
  • 阿里巴巴网站怎么做全屏大图公司建设内容是什么
  • 网站建设维护属于什么专业网站开发常用的谷歌插件
  • html页面能做成网站吗类似情侣空间的网站开发
  • 做招聘网站需要什么资质苏州行业网络推广排名稳定
  • 自适应滤波器减轻光照对卷心菜识别准确性影响
  • 宁波外贸网站制作公司必应网站收录在哪
  • 元宇宙与文化传承:数字技术保护文化遗产
  • OpenSearch/Ingest Pipeline + k-NN 在 ABP 的“双索引”落地
  • 手机网站 手机app微信会员卡管理系统
  • 做网站详细步骤非法网站开发
  • 北京建设执业网站网站开发电脑内存要多少钱
  • 怎么做晒鱼的网站电话手表网站
  • 深圳北网站建设王也天与葛优
  • 太原网站建设方案做简历用的网站
  • 重庆的做网站公司wap网站浏览器
  • 历史看过的网站网站建设工作
  • 如何学会建网站wordpress电源模板
  • 如何套用别人网站模板深圳市网站维护
  • 低价建网站深圳竞价网站
  • 建设网站带后台管理wordpress替换文字
  • 网站认证必须做么网站备案电话号码
  • 全文索引可能涉及的技术
  • 郑州网站建设特色asp网站制作设计教程
  • 西安营销型网站建设动力无限上海网站建设的公司
  • 手机可以访问的网站怎么做芜湖百度seo
  • 专注高密做网站哪家强百度首页优化