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

微服务项目->在线oj系统(Java-Spring)--C端用户(超详细)

C端登录注册功能

项目结构

发送验证码

由于我们前面已经讲了很多后端代码相关内容了,相信大家学到这里的时候应该对代码很熟悉了,就不做详细讲解,看代码直接懂就好了

Controller

import com.bite.common.core.controller.BaseController;
import com.bite.common.core.domain.R;
import com.bite.friend.model.DTO.UserDTO;
import com.bite.friend.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
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;@RestController
@RequestMapping("/user")
public class UserController extends BaseController {@Autowiredprivate IUserService userService;//  /user/sendCode@PostMapping("sendCode")public R<Void> sendCode(@RequestBody UserDTO userDTO) {return toR(userService.sendCode(userDTO)) ;}
}

DTO

@Getter
@Setter
public class UserDTO {private String phone;private String code;
}

Service

我们这里首先对传过来的手机号进行验证验证,看是否符合一个手机号的格式,然后生成一个随机验证码,至于发送验证码,我们需要用到阿里云短信服务(但是,我们发现,阿里云的短信服务居然不允许个人使用了)

import cn.hutool.core.util.RandomUtil;
import com.bite.common.core.enums.ResultCode;
import com.bite.common.security.EXCEPTION.ServiceException;
import com.bite.friend.model.DTO.UserDTO;
import com.bite.friend.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.regex.Matcher;
import java.util.regex.Pattern;@Service
@Slf4j
public class UserServiceImpl implements IUserService {@Overridepublic int sendCode(UserDTO userDTO) {if (!checkPhone(userDTO.getPhone())) {throw new ServiceException(ResultCode.FAILED_USER_PHONE);}String phoneCodeKey = getPhoneCodeKey(userDTO.getPhone());Long expire = redisService.getExpire(phoneCodeKey, TimeUnit.SECONDS);if (expire != null && (phoneCodeExpiration * 60 - expire) < 60 ){throw new ServiceException(ResultCode.FAILED_FREQUENT);}//每天的验证码获取次数有一个限制  50次  第二天  计数清0 重新开始计数     计数  怎么存  存在哪//操作这个次数数据频繁   、 不需要存储、  记录的次数 有有效时间的(当天有效) redis  String  key:c:t:手机号//获取已经请求的次数  和50 进行比较     如果大于限制抛出异常。如果不大于限制,正常执行后续逻辑,并且将获取计数 + 1String codeTimeKey = getCodeTimeKey(userDTO.getPhone());Long sendTimes = redisService.getCacheObject(codeTimeKey, Long.class);if (sendTimes != null && sendTimes >= sendLimit) {throw new ServiceException(ResultCode.FAILED_TIME_LIMIT);}String code=RandomUtil.randomNumbers(6);System.out.println("密码:"+code);redisService.setCacheObject(phoneCodeKey, code, phoneCodeExpiration, TimeUnit.MINUTES);redisService.increment(codeTimeKey);if (sendTimes == null) {  //说明是当天第一次发起获取验证码的请求long seconds = ChronoUnit.SECONDS.between(LocalDateTime.now(),LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0));redisService.expire(codeTimeKey, seconds, TimeUnit.SECONDS);}return 1;}
}

我们这里需要保证几点:

1.检查这个手机号是否为正确的

2.获得当前验证码还剩多少秒,如果一分钟内有5次以上,则抛出异常

3.查看一天内发送了多少次,如果超过一定限度则抛出异常,每次发送后++;

mapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bite.friend.model.User;public interface UserMapper  extends BaseMapper<User> {}

User

package com.bite.friend.model;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bite.common.core.domain.BaseEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
@TableName("tb_user")
public class User extends BaseEntity {@JsonSerialize(using = ToStringSerializer.class)@TableId(value = "USER_ID", type = IdType.ASSIGN_ID)private Long userId;private String nickName;private String headImage;private Integer sex;private String phone;private String code;private String email;private String wechat;private String schoolName;private String majorName;private String introduce;private Integer status;
}

配置文件bootstrap.yml

server:port: 9202
# Spring
spring:application:# 应用名称name: oj-friendprofiles:# 环境配置active: localcloud:nacos:discovery:namespace: 78015f37-f9d0-4c31-8970-cc017a0923ffserver-addr: http://localhost:8848config:namespace: 78015f37-f9d0-4c31-8970-cc017a0923ffserver-addr: http://localhost:8848file-extension: yaml

nacos配置文件

除此之外我们还需要配置gateway的白名单以及路由

server:port: 19090
spring:data:redis:host: localhostpassword: 123456cloud:gateway:routes:# 管理模块- id: oj-systemuri: lb://oj-systempredicates:- Path=/system/**filters:- StripPrefix=1- id: oj-frienduri: lb://oj-friend  # 假设friend服务的注册名是friend-servicepredicates:- Path=/friend/**       # 匹配以/friend开头的请求filters:- StripPrefix=1        
security:ignore:whites:- /system/sysUser/login- /friend/user/sendCode- /friend/user/code/login- /friend/user/test
jwt:secret: zxsksjdjoss

登录注册接口

登录流程设计

  • 点击登录按钮后,系统会执行以下验证和操作流程
  • 验证码校验阶段: 从Redis缓存中获取当前手机号对应的验证码,与用户输入的验证码进行比对。若验证失败则立即终止流程
  • 用户验证阶段: 通过手机号查询数据库用户表。若用户存在则识别为老用户,不存在则进入新用户注册流程
  • 老用户处理: 生成新的访问令牌(Token),将完整的用户信息存入Redis缓存,设置合理的过期时间
  • 新用户处理: 自动完成新用户注册,为手机号创建基础用户记录并保存到数据库。随后执行与老用户相同的令牌生成和缓存操作

 @Overridepublic String codeLogin(String phone, String code) {checkCode(phone, code);User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));if (user == null) {  //新用户//注册逻辑user = new User();user.setPhone(phone);user.setStatus(UserStatus.Normal.getValue());user.setCreateBy(Contants.SYSTEM_USER_ID);userMapper.insert(user);}return tokenService.createToken(user.getUserId(), secret, UserIdentity.ORDINARY.getCode(), user.getNickName(), user.getHeadImage());}

其余方法

方法1: 验证码校对

方法2: 获得手机号::密码key

方法3:获得密码::时间key

方法4:校验手机号格式是否正确

    private void checkCode(String phone, String code) {String phoneCodeKey = getPhoneCodeKey(phone);String cacheCode = redisService.getCacheObject(phoneCodeKey, String.class);if (StrUtil.isEmpty(cacheCode)) {throw new ServiceException(ResultCode.FAILED_INVALID_CODE);}if (!cacheCode.equals(code)) {throw new ServiceException(ResultCode.FAILED_ERROR_CODE);}//验证码比对成功redisService.deleteObject(phoneCodeKey);}private String getPhoneCodeKey(String phone) {return CacheConstants.PHONE_CODE_KEY + phone;}private String getCodeTimeKey(String phone) {return CacheConstants.CODE_TIME_KEY + phone;}public static boolean checkPhone(String phone) {Pattern regex = Pattern.compile("^1[2|3|4|5|6|7|8|9][0-9]\\d{8}$");Matcher m = regex.matcher(phone);return m.matches();}

退出登录

点击退出按钮时,系统会先检查token是否为空。若token存在且包含预设前缀,则先去除前缀,再执行token删除操作。具体流程为:解析token获取对应的redis键值,然后在redis中删除该键值。

  @DeleteMapping("/logout")public R<Void> logout(@RequestHeader(HttpConstants.AUTHENTICATION) String token) {return toR(userService.logout(token));}
 @Overridepublic boolean logout(String token) {if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);}return tokenService.deleteLoginUser(token, secret);}

获得信息

从请求头中提取token并解析后,若未能从Redis获取到对应的LoginUser信息,则返回错误响应;若成功获取,则将LoginUser中的相关数据映射到返回给前端的VO对象中。

  @GetMapping("/info")public R<LoginUserVO> info(@RequestHeader(HttpConstants.AUTHENTICATION) String token) {return userService.info(token);}
 @Overridepublic R<LoginUserVO> info(String token) {if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);}LoginUser loginUser = tokenService.getLoginUser(token, secret);System.out.println(loginUser);if (loginUser == null) {return R.fail();}LoginUserVO loginUserVO = new LoginUserVO();loginUserVO.setNickName(loginUser.getNickName());loginUserVO.setHeadImage(loginUser.getHeadImage());System.out.println(loginUserVO);return R.ok(loginUserVO);}

前端代码

Login.vue

<template><div class="login-page"><div class="orange"> </div><div class="blue"></div><div class="blue small"></div><div class="login-box"><div class="logo-box"><img src="@/assets/logo.png"><div><div class="sys-name">比特OJ</div><div class="sys-sub-name">帮助100万大学生就业</div></div></div><div class="form-box-title"><span>验证码登录</span></div><div class="form-box"><div class="form-item"><img src="@/assets/images/shouji.png"><el-input v-model="mobileForm.phone" type="text" placeholder="请输入手机号" /></div><div class="form-item"><img src="@/assets/images/yanzhengma.png"><el-input style="width:134px" v-model="mobileForm.code" type="text" placeholder="请输入验证码" /><div class="code-btn-box" @click="getCode" :disabled="isCodeBtnDisabled"><span>{{ txt }}</span></div></div><div class="submit-box" @click="loginFun">登录/注册</div></div><div class="gray-bot"><p>注册或点击登录代表您同意 <span>服务条款</span> 和 <span>隐私协议</span></p></div></div></div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { setToken } from '@/utils/cookie'
import { sendCodeService, codeLoginService } from '@/api/user'
import router from '@/router'// 验证码登录表单
let mobileForm = reactive({phone: '',code: ''
})
let txt = ref('获取验证码')
let timer = null
// 新增:控制按钮是否可点击的状态
let isCodeBtnDisabled = ref(false)async function getCode() {// 点击后立即禁用按钮isCodeBtnDisabled.value = trueawait sendCodeService(mobileForm)txt.value = '59s'let num = 59timer = setInterval(() => {num--if (num < 1) {txt.value = '重新获取验证码'clearInterval(timer)// 倒计时结束,启用按钮isCodeBtnDisabled.value = false} else {txt.value = num + 's'}}, 1000)
}async function loginFun() {const loginRef = await codeLoginService(mobileForm)setToken(loginRef.data)router.push('/c-oj/home')
}
</script>
<style lang="scss" scoped>
.login-page {width: 100vw;height: 100vh;position: relative;margin-top: -60px;margin-left: -20px;overflow: hidden;.login-box {width: 600px;height: 604px;background: #FFFFFF;box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);border-radius: 10px;opacity: 0.9;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 2;padding: 0 72px;padding-top: 50px;overflow: hidden;.logo-box {display: flex;align-items: center;&.refister-logo {margin-bottom: 56px;}img {width: 68px;height: 68px;margin-right: 16px;}.sys-name {height: 33px;font-family: PingFangSC, PingFang SC;font-weight: 600;font-size: 24px;color: #222222;line-height: 33px;margin-bottom: 13px;}.sys-sub-name {height: 22px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 16px;color: #222222;line-height: 22px;}}.form-box-title {height: 116px;display: flex;align-items: center;span {font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 24px;color: #000000;line-height: 33px;display: block;height: 33px;margin-right: 40px;position: relative;letter-spacing: 1px;cursor: pointer;&.active {font-weight: bold;&::before {position: absolute;content: '';bottom: -13px;left: 0;width: 100%;height: 5px;background: #32C5FF;border-radius: 10px;}}}}.gray-bot {position: absolute;left: 0;text-align: center;margin-top: 56px;width: 100%;height: 50px;background: #FAFAFA;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 14px;color: #666666;line-height: 50px;p {margin: 0;}span {color: #32C5FF;cursor: pointer;}}:deep(.form-box) {.submit-box {margin-top: 90px;width: 456px;height: 48px;background: #96E1FE;border-radius: 8px;cursor: pointer;display: flex;justify-content: center;align-items: center;font-family: PingFangSC, PingFang SC;font-weight: 600;font-size: 16px;color: #FFFFFF;letter-spacing: 1px;&.refister-submit {margin-top: 72px;}&:hover {background: #32C5FF;}}.form-item {display: flex;align-items: center;width: 456px;height: 48px;background: #F8F8F8;border-radius: 8px;margin-bottom: 30px;position: relative;.code-btn-box {position: absolute;right: 0;width: 151px;height: 48px;background: #32C5FF;border-radius: 8px;top: 0;display: flex;align-items: center;justify-content: center;cursor: pointer;span {font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 16px;color: #FFFFFF;}}.error-tip {position: absolute;width: 140px;text-align: right;padding-right: 12px;height: 20px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 14px;color: #FD4C40;line-height: 20px;right: 0;&.bottom {right: 157px;}}.el-input {width: 380px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 16px;color: #222222;}.el-input__wrapper {border: none;box-shadow: none;background: transparent;width: 230px;padding-left: 0;}img {width: 24px;height: 24px;margin: 0 18px;}}}}&::after {position: absolute;top: 0;left: 0;height: 100vh;bottom: 0;right: 0;background: rgba(255, 255, 255, .8);z-index: 1;content: '';}.orange {background: #F0714A;width: 498px;height: 498px;border-radius: 50%;background: #F0714A;opacity: 0.67;filter: blur(50px);left: 14.2%;top: 41%;position: absolute;}.blue {width: 334px;height: 334px;background: #32C5FF;opacity: 0.67;filter: blur(50px);left: 14.2%;top: 42%;position: absolute;top: 16.3%;left: 80.7%;&.small {width: 186px;height: 186px;top: 8.2%;left: 58.2%;}}
}
</style>

Template和style部分的代码我们不做多余介绍,我们介绍一些js部分代码

Home.vue

<template><div class="oj-main-layout"><div class="oj-main-layout-header"><div class="oj-main-layout-nav"><Navbar></Navbar></div></div><div><img src="@/assets/images/log-banner.png" class="banner-img"></div></div>
</template><script setup>
import Navbar from '@/components/Navbar.vue'
</script><style lang="scss" scoped>
.el-main {padding: 0;
}.oj-main-layout {padding-top: 20px;.banner-img {max-width: 1520px;margin: 0 auto;border-radius: 16px;width: "100%"}.oj-main-layout-header {height: 60px;position: absolute;width: 100%;background: #fff;left: 0;top: 0;z-index: 3;overflow: hidden;}.oj-main-layout-nav {max-width: 1520px;min-width: 100%;margin: 0 auto;height: 60px;background: #fff;}.oj-ship-banner {max-width: 1520px;min-width: 1520;margin: 0 auto;width: 100%;height: 100%;height: 350px;color: #ffffff;background: url("@/assets/index_bg.png") left top no-repeat;background-size: cover;overflow: hidden;}
}
</style>

Navbar.vue

<template><div class="oj-navbar"><div class="oj-navbar-menus"><img class="oj-navbar-logo" src="@/assets/logo.png" /><el-menu class="oj-navbar-menu" mode="horizontal"><el-menu-item>题库</el-menu-item><el-menu-item>竞赛</el-menu-item></el-menu></div><div class="oj-navbar-users"><img v-if="isLogin" class="oj-message" @click="goMessage" src="@/assets/message/message.png" /><el-dropdown v-if="isLogin"><div class="oj-navbar-name"><img class="oj-head-image" v-if="isLogin"  :src="userInfo.headImage" /><span>{{ userInfo.nickName }}</span></div><template #dropdown><el-dropdown-menu><el-dropdown-item @click="goUserInfo"><div class="oj-navabar-item"><span>个⼈中⼼</span></div></el-dropdown-item><el-dropdown-item @click="goMyExam"><div class="oj-navabar-item"><span>我的⽐赛</span></div></el-dropdown-item><el-dropdown-item><div class="oj-navabar-item"><span @click="handleLogout">退出登录</span></div></el-dropdown-item></el-dropdown-menu></template></el-dropdown><span class="oj-navbar-login-btn" v-if="!isLogin" @click="goLogin">登录</span></div></div>
</template><script setup>
import { ref, reactive, onMounted } from 'vue'
import router from '@/router';
import { getToken, removeToken } from '@/utils/cookie'
import { logoutService, getUserInfoService } from '@/api/user'
import { ElMessageBox } from 'element-plus'; // 补充可能遗漏的导入const userInfo = reactive({nickName: '',headImage: ''
})
// 当前登录状态
const isLogin = ref(false)// 检查登录状态
async function checkLogin() {if (getToken()) {// 实际项目中应调用接口验证token有效性const userInfoRes = await getUserInfoService()console.log(userInfoRes)userInfoRes.data.headImage = `/${userInfoRes.data.headImage}`Object.assign(userInfo, userInfoRes.data)console.log(userInfo)isLogin.value = true}
}
checkLogin()// 退出登录
async function handleLogout() {await ElMessageBox.confirm('确认退出','温馨提⽰',{confirmButtonText: '确认',cancelButtonText: '退出',type: 'warning',})await logoutService()removeToken()isLogin.value = false
}// 跳转到登录页
function goLogin() {router.push('/c-oj/login')
}// 以下方法需要补充实现
function goMessage() {// 消息页面跳转逻辑
}function goUserInfo() {// 个人中心跳转逻辑
}function goMyExam() {// 我的比赛跳转逻辑
}
</script><style lang="scss" scoped>
.oj-navbar {display: flex;justify-content: space-between;align-items: center;padding: 0 20px;box-sizing: border-box;max-width: 1520px;margin: 0 auto;.oj-navbar-menus {display: flex;align-items: center;height: 50px;.el-menu-item {font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 20px;color: #222222;line-height: 28px;text-align: center;width: 42px;text-align: left;margin-right: 25px;}}.oj-navbar-logo {width: 38px;height: 38px;background: #32C5FF;border-radius: 8px;cursor: pointer;object-fit: contain;margin-right: 59px;}.oj-navbar-menu {width: 600px;border: none;.el-menu-item {font-size: 16px;font-weight: 500;background-color: transparent !important;transition: none;border: none;line-height: 60px;}}.oj-navbar-users {display: flex;align-items: center;}.oj-navbar-login-btn {line-height: 60px;display: inline-block;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 18px;color: #222222;text-align: center;cursor: pointer;.line {display: inline-block;width: 25px;}}.oj-message {cursor: pointer;margin-top: 15px;}.oj-head-image {width: 30px;height: 30px;border-radius: 30px;margin-right: 10px;}.oj-navbar-name {cursor: pointer;margin-top: 15px;font-weight: 400;color: #000;margin-left: 15px;font-size: 20px;width: 100px;display: flex;align-items: center;}.oj-navabar-item {display: flex;align-items: center;justify-content: center;padding: 0 32px;}
}
</style>

request.js

和之前学的一样,不做多余解释

import axios from 'axios'
import { getToken, removeToken } from './cookie'
import router from '@/router';//不同的功能,通过axios请求的是不同接口的地址
//127.0.0.1:19090
const service = axios.create({baseURL:"/dev-api",timeout:5000,
})
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";//请求拦截器
service.interceptors.request.use((config) => {if (getToken()) {config.headers["Authorization"] = "Bearer " + getToken();}return config;},(error) => {console.log(error)Promise.reject(error);}
);//响应拦截器
service.interceptors.response.use((res) => {  //res : 响应数据// 未设置状态码则默认成功状态const code = res.data.code;const msg = res.data.msg;if (code === 3001) {ElMessage.error(msg);removeToken()return Promise.reject(new Error(msg));} else if (code !== 1000) {ElMessage.error(msg);return Promise.reject(new Error(msg));} else {return Promise.resolve(res.data);}},(error) => {return Promise.reject(error);}
);export default service

cookie.js

和之前系统用户那里一样,不做多余解释

import Cookies from "js-cookie";
const TokenKey = "Oj-c-Token";export function getToken() {return Cookies.get(TokenKey);
}export function setToken(token) {return Cookies.set(TokenKey, token);
}export function removeToken() {return Cookies.remove(TokenKey);
}

index.js

路由功能,不做多余解释

import { createRouter, createWebHistory } from 'vue-router'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: "/",redirect: '/c-oj/home',},{path: "/c-oj/home",name: "home",component: () => import("@/views/Home.vue"),},{path: "/c-oj/login",name: "login",component: () => import("@/views/Login.vue"),},{path: "//c-oj/test",name: "test",component: () => import("@/views/Test.vue"),}],
})export default router

vite.config.js

配置功能,不多余解释

import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},server: {proxy: {"/dev-api": {target: "http://127.0.0.1:19090/friend",rewrite: (p) => p.replace(/^\/dev-api/, ""),},},},
})

效果展示

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

相关文章:

  • <从零基础到精通JavaScript>1.2 变量声明 (let const)
  • 方差齐性(Homoscedasticity):概念、检验方法与处理策略
  • html个人网站制作wordpress按分类设置seo
  • 网站图片水印青海住房和城乡建设部网站
  • 网站备案了有什么好处wordpress chianz
  • 网站开发现在主要用什么语言企业网站运维
  • Windows环境下,源码启动+本地部署和启动开源项目Ragflow失败SRE模块
  • 高陵微网站建设北京seo网站结构优化
  • 【Leetcode hot 100】51.N皇后
  • 虚拟空间能建多个网站浙江省建设会计协会网站首页
  • 中英文网站域名的区别外贸网站开发哪家好
  • 网站加载不出来是什么原因建筑企业
  • 湖北省住房城乡建设厅网站查wordpress修改首页模板文件名
  • 个人做百度云下载网站吗c sql网站开发
  • 山东省建设公司网站怎样注册自己的微信小程序
  • 成都企业网站开发公司十堰网络销售
  • pve8.3安装win11
  • 网站设计的基本流程是什么什么行业 网站
  • AUTOSAR进阶图解==>AUTOSAR_TR_InteroperabilityOfAutosarTools
  • 网站建设与制十堰网站建设价格
  • PCIe协议之低功耗篇之 L0s 状态(一)
  • Uav toolbox使用
  • 网站架构企业收费标准网站优化 无需定金
  • 网站设计指南wordpress 添加
  • JavaScript 与 TypeScript 深度解析:特性、区别、联系与实践指南
  • 吴江区网站建设做汽车配件生意的网站
  • 做网站需要用什么技术软件维护有哪些内容
  • 10.6总结
  • 在哪里安装wordpress嘉兴seo网站排名
  • 电子商务网站建设与维护期末答案桂平百度seo