feat(auth): 实现扫码登录功能并优化邮件模板

- 新增 QrLoginController、QrLoginService、QrLoginData等类实现扫码登录功能
- 更新邮件模板中的公司名称、网址等信息
- 添加 JWT 配置项
- 优化应用配置文件,启用 Jackson 对 Java 8 时间类型的支持
This commit is contained in:
2025-09-01 11:32:01 +08:00
parent c23302d9d3
commit 8529a826d7
20 changed files with 689 additions and 36 deletions

View File

@@ -98,7 +98,7 @@ emailTemplateUtil.sendNotificationEmailWithAction(
"您的订单已发货",
"user@example.com",
1001,
"https://www.gxwebsoft.com/orders/12345",
"https://websoft.top/orders/12345",
"查看订单"
);
```

View File

@@ -47,6 +47,12 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jackson-datatype-jsr310 for Java 8 time support -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- spring-boot-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -0,0 +1,88 @@
package com.gxwebsoft.auto.controller;
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
import com.gxwebsoft.auto.service.QrLoginService;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.ApiResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 认证模块
*
* @author 科技小王子
* @since 2025-03-06 22:50:25
*/
@Tag(name = "认证模块")
@RestController
@RequestMapping("/api/qr-login")
public class QrLoginController extends BaseController {
@Autowired
private QrLoginService qrLoginService;
/**
* 生成扫码登录token
*/
@Operation(summary = "生成扫码登录token")
@PostMapping("/generate")
public ApiResult<?> generateQrLoginToken() {
try {
QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken();
return success("生成成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 检查扫码登录状态
*/
@Operation(summary = "检查扫码登录状态")
@GetMapping("/status/{token}")
public ApiResult<?> checkQrLoginStatus(
@Parameter(description = "扫码登录token") @PathVariable String token) {
try {
QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
return success("查询成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 确认扫码登录
*/
@Operation(summary = "确认扫码登录")
@PostMapping("/confirm")
public ApiResult<?> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
try {
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
return success("确认成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 扫码操作(可选接口,用于移动端扫码后更新状态)
*/
@Operation(summary = "扫码操作")
@PostMapping("/scan/{token}")
public ApiResult<?> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
try {
boolean result = qrLoginService.scanQrCode(token);
return success("操作成功", result);
} catch (Exception e) {
return fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,25 @@
package com.gxwebsoft.auto.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 扫码登录确认请求
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@Schema(description = "扫码登录确认请求")
public class QrLoginConfirmRequest {
@Schema(description = "扫码登录token")
@NotBlank(message = "token不能为空")
private String token;
@Schema(description = "用户ID")
private Integer userId;
}

View File

@@ -0,0 +1,55 @@
package com.gxwebsoft.auto.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 扫码登录数据模型
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QrLoginData {
/**
* 扫码登录token
*/
private String token;
/**
* 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期
*/
private String status;
/**
* 用户ID(扫码确认后设置)
*/
private Integer userId;
/**
* 用户名(扫码确认后设置)
*/
private String username;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* JWT访问令牌(确认后生成)
*/
private String accessToken;
}

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.auto.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 扫码登录生成响应
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "扫码登录生成响应")
public class QrLoginGenerateResponse {
@Schema(description = "扫码登录token")
private String token;
@Schema(description = "二维码内容")
private String qrCode;
@Schema(description = "过期时间(秒)")
private Long expiresIn;
}

View File

@@ -0,0 +1,32 @@
package com.gxwebsoft.auto.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 扫码登录状态响应
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "扫码登录状态响应")
public class QrLoginStatusResponse {
@Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期")
private String status;
@Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
private String accessToken;
@Schema(description = "用户信息(仅在confirmed状态时返回)")
private Object userInfo;
@Schema(description = "剩余过期时间(秒)")
private Long expiresIn;
}

View File

@@ -0,0 +1,46 @@
package com.gxwebsoft.auto.service;
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
/**
* 扫码登录服务接口
*
* @author 科技小王子
* @since 2025-08-31
*/
public interface QrLoginService {
/**
* 生成扫码登录token
*
* @return QrLoginGenerateResponse
*/
QrLoginGenerateResponse generateQrLoginToken();
/**
* 检查扫码登录状态
*
* @param token 扫码登录token
* @return QrLoginStatusResponse
*/
QrLoginStatusResponse checkQrLoginStatus(String token);
/**
* 确认扫码登录
*
* @param request 确认请求
* @return QrLoginStatusResponse
*/
QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
/**
* 扫码操作(更新状态为已扫码)
*
* @param token 扫码登录token
* @return boolean
*/
boolean scanQrCode(String token);
}

View File

@@ -0,0 +1,204 @@
package com.gxwebsoft.auto.service.impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.auto.dto.*;
import com.gxwebsoft.auto.service.QrLoginService;
import com.gxwebsoft.common.core.security.JwtSubject;
import com.gxwebsoft.common.core.security.JwtUtil;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
/**
* 扫码登录服务实现
*
* @author 科技小王子
* @since 2025-08-31
*/
@Slf4j
@Service
public class QrLoginServiceImpl implements QrLoginService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserService userService;
@Value("${config.jwt.secret:websoft-jwt-secret-key-2025}")
private String jwtSecret;
@Value("${config.jwt.expire:86400}")
private Long jwtExpire;
@Override
public QrLoginGenerateResponse generateQrLoginToken() {
// 生成唯一的扫码登录token
String token = UUID.randomUUID().toString(true);
// 创建扫码登录数据
QrLoginData qrLoginData = new QrLoginData();
qrLoginData.setToken(token);
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
qrLoginData.setCreateTime(LocalDateTime.now());
qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL));
// 存储到Redis设置过期时间
String redisKey = QR_LOGIN_TOKEN_KEY + token;
redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
log.info("生成扫码登录token: {}", token);
// 构造二维码内容(这里可以是前端登录页面的URL + token参数)
String qrCodeContent = "qr-login:" + token;
return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL);
}
@Override
public QrLoginStatusResponse checkQrLoginStatus(String token) {
if (StrUtil.isBlank(token)) {
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
}
String redisKey = QR_LOGIN_TOKEN_KEY + token;
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
}
// 检查是否过期
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
// 删除过期的token
redisUtil.delete(redisKey);
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
}
// 计算剩余过期时间
long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
QrLoginStatusResponse response = new QrLoginStatusResponse();
response.setStatus(qrLoginData.getStatus());
response.setExpiresIn(expiresIn);
// 如果已确认返回token和用户信息
if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) {
response.setAccessToken(qrLoginData.getAccessToken());
// 获取用户信息
if (qrLoginData.getUserId() != null) {
User user = userService.getByIdRel(qrLoginData.getUserId());
if (user != null) {
// 清除敏感信息
user.setPassword(null);
response.setUserInfo(user);
}
}
// 确认后删除token防止重复使用
redisUtil.delete(redisKey);
}
return response;
}
@Override
public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
String token = request.getToken();
Integer userId = request.getUserId();
if (StrUtil.isBlank(token) || userId == null) {
throw new RuntimeException("参数不能为空");
}
String redisKey = QR_LOGIN_TOKEN_KEY + token;
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
throw new RuntimeException("扫码登录token不存在或已过期");
}
// 检查是否过期
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
redisUtil.delete(redisKey);
throw new RuntimeException("扫码登录token已过期");
}
// 获取用户信息
User user = userService.getByIdRel(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 检查用户状态
if (user.getStatus() != null && user.getStatus() != 0) {
throw new RuntimeException("用户已被冻结");
}
// 生成JWT token
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret);
// 更新扫码登录数据
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
qrLoginData.setUserId(userId);
qrLoginData.setUsername(user.getUsername());
qrLoginData.setAccessToken(accessToken);
// 更新Redis中的数据
redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token
log.info("用户 {} 确认扫码登录token: {}", user.getUsername(), token);
// 清除敏感信息
user.setPassword(null);
return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L);
}
@Override
public boolean scanQrCode(String token) {
if (StrUtil.isBlank(token)) {
return false;
}
String redisKey = QR_LOGIN_TOKEN_KEY + token;
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
return false;
}
// 检查是否过期
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
redisUtil.delete(redisKey);
return false;
}
// 只有pending状态才能更新为scanned
if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
// 计算剩余过期时间
long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
log.info("扫码登录token {} 状态更新为已扫码", token);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
package com.gxwebsoft.common.core.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Jackson配置类
* 解决Java 8时间类型序列化问题
*
* @author WebSoft
* @since 2024-08-28
*/
@Configuration
public class JacksonConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 注册JavaTimeModule
mapper.registerModule(new JavaTimeModule());
// 禁用将日期写为时间戳
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 禁用将日期时间戳写为纳秒
mapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
return mapper;
}
}

View File

@@ -49,7 +49,7 @@ public class SwaggerConfig {
.title(config.getSwaggerTitle())
.description(config.getSwaggerDescription())
.version(config.getSwaggerVersion())
.contact(new Contact("科技小王子","https://www.gxwebsoft.com","170083662@qq.com"))
.contact(new Contact("科技小王子","https://websoft.top","170083662@qq.com"))
.termsOfServiceUrl("https://server.gxwebsoft.com/api")
.build();
}

View File

@@ -27,6 +27,14 @@ public class RedisConstants {
// 扫码登录相关key
public static final String QR_LOGIN_TOKEN_KEY = "qr-login:token:"; // 扫码登录token前缀
public static final Long QR_LOGIN_TOKEN_TTL = 300L; // 扫码登录token过期时间(5分钟)
public static final String QR_LOGIN_STATUS_PENDING = "pending"; // 等待扫码
public static final String QR_LOGIN_STATUS_SCANNED = "scanned"; // 已扫码
public static final String QR_LOGIN_STATUS_CONFIRMED = "confirmed"; // 已确认
public static final String QR_LOGIN_STATUS_EXPIRED = "expired"; // 已过期
// 哗啦啦key
public static final String getAllShop = "allShop";
public static final String getBaseInfo = "baseInfo";

View File

@@ -39,6 +39,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.permitAll()
.antMatchers(
"/api/login",
"/api/qr-login/**",
"/api/loginByUserId",
"/api/register",
"/api/superAdminRegister",

View File

@@ -0,0 +1,116 @@
package com.gxwebsoft.common.system.controller;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.util.EmailTemplateUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 邮件测试控制器
* 用于测试邮件模板功能
*
* @author WebSoft
* @since 2024-08-28
*/
@Api(tags = "邮件测试")
@RestController
@RequestMapping("/api/email-test")
public class EmailTestController extends BaseController {
@Resource
private EmailTemplateUtil emailTemplateUtil;
@ApiOperation("测试注册成功邮件")
@PostMapping("/register-success")
public ApiResult<?> testRegisterSuccessEmail(@RequestParam String email) {
try {
emailTemplateUtil.sendRegisterSuccessEmail(
"测试用户",
"13800138000",
"TestPassword123",
email,
getTenantId()
);
return success("注册成功邮件发送成功");
} catch (Exception e) {
return fail("邮件发送失败: " + e.getMessage());
}
}
@ApiOperation("测试密码重置邮件")
@PostMapping("/password-reset")
public ApiResult<?> testPasswordResetEmail(@RequestParam String email) {
try {
emailTemplateUtil.sendPasswordResetEmail(
"测试用户",
"13800138000",
"NewPassword456",
email,
getTenantId()
);
return success("密码重置邮件发送成功");
} catch (Exception e) {
return fail("邮件发送失败: " + e.getMessage());
}
}
@ApiOperation("测试通知邮件")
@PostMapping("/notification")
public ApiResult<?> testNotificationEmail(@RequestParam String email,
@RequestParam(required = false) String title,
@RequestParam(required = false) String content) {
try {
String emailTitle = title != null ? title : "WebSoft系统通知";
String emailContent = content != null ? content : "这是一条测试通知消息,用于验证邮件模板功能是否正常工作。";
emailTemplateUtil.sendNotificationEmailWithAction(
emailTitle,
emailContent,
email,
getTenantId(),
"https://www.gxwebsoft.com",
"访问官网"
);
return success("通知邮件发送成功");
} catch (Exception e) {
return fail("邮件发送失败: " + e.getMessage());
}
}
@ApiOperation("测试安全提醒邮件")
@PostMapping("/security-alert")
public ApiResult<?> testSecurityAlertEmail(@RequestParam String email) {
try {
emailTemplateUtil.sendSecurityAlertEmail(
"测试用户",
"13800138000",
email,
getTenantId(),
"异地登录检测"
);
return success("安全提醒邮件发送成功");
} catch (Exception e) {
return fail("邮件发送失败: " + e.getMessage());
}
}
@ApiOperation("测试系统维护通知邮件")
@PostMapping("/maintenance")
public ApiResult<?> testMaintenanceEmail(@RequestParam String email) {
try {
emailTemplateUtil.sendMaintenanceNotificationEmail(
email,
getTenantId(),
"2024年8月28日 23:00-24:00",
"1小时"
);
return success("维护通知邮件发送成功");
} catch (Exception e) {
return fail("邮件发送失败: " + e.getMessage());
}
}
}

View File

@@ -157,7 +157,7 @@ public class EmailTemplateUtil {
String title = "WebSoft账户安全提醒";
String content = "您的账户发生了以下安全事件:" + event + "。如果这不是您本人的操作,请立即联系客服并修改密码。";
String infoMessage = "为了保障您的账户安全,建议您定期修改密码并开启双重验证。";
String actionUrl = "https://www.gxwebsoft.com/security";
String actionUrl = "https://websoft.top/security";
String actionText = "查看安全设置";
sendNotificationEmail(title, content, email, tenantId, "尊敬的用户", infoMessage, actionUrl, actionText);
@@ -191,7 +191,7 @@ public class EmailTemplateUtil {
public void sendOrderStatusEmail(String username, String orderNo, String status, String email, Integer tenantId) {
String title = "WebSoft订单状态更新";
String content = "您的订单 " + orderNo + " 状态已更新为:" + status + "";
String actionUrl = "https://www.gxwebsoft.com/orders/" + orderNo;
String actionUrl = "https://websoft.top/orders/" + orderNo;
String actionText = "查看订单详情";
sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText);

View File

@@ -41,6 +41,11 @@ spring:
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
serialization:
write-dates-as-timestamps: false
write-date-timestamps-as-nanoseconds: false
deserialization:
read-date-timestamps-as-nanoseconds: false
# 设置上传文件大小
servlet:
@@ -101,6 +106,10 @@ config:
bucketDomain: https://oss.wsdns.cn
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
# JWT配置
jwt:
secret: websoft-jwt-secret-key-2025-dev-environment
expire: 86400 # token过期时间(秒) 24小时
# 证书配置
certificate:

View File

@@ -6,19 +6,19 @@ email:
# 品牌信息
brand:
name: "WebSoft"
company: "南宁网宿信息科技有限公司"
company: "南宁网宿信息科技有限公司"
description: "企业级数字化解决方案"
website: "https://www.gxwebsoft.com"
website: "https://websoft.top"
support_email: "170083662@qq.com"
logo_url: "https://www.gxwebsoft.com/logo.png"
logo_url: "https://websoft.top/logo.png"
# 链接配置
links:
login: "https://www.gxwebsoft.com/login"
help: "https://www.gxwebsoft.com/help"
contact: "https://www.gxwebsoft.com/contact"
security: "https://www.gxwebsoft.com/security"
login: "https://websoft.top/login"
help: "https://websoft.top/help"
contact: "https://websoft.top/contact"
security: "https://websoft.top/security"
# 模板列表
templates:
register-success:
@@ -26,37 +26,37 @@ email:
description: "用户注册成功后发送的欢迎邮件"
file: "register-success.html"
subject: "恭喜您的WebSoft账号已注册成功"
password-reset:
name: "密码重置邮件"
description: "用户密码重置后发送的通知邮件"
file: "password-reset.html"
subject: "WebSoft密码重置通知"
notification:
name: "通用通知邮件"
description: "系统通知、公告等通用邮件模板"
file: "notification.html"
subject: "WebSoft系统通知"
security-alert:
name: "安全提醒邮件"
description: "账户安全相关的提醒邮件"
file: "notification.html"
subject: "WebSoft账户安全提醒"
maintenance:
name: "系统维护通知"
description: "系统维护时发送的通知邮件"
file: "notification.html"
subject: "WebSoft系统维护通知"
order-status:
name: "订单状态更新"
description: "订单状态变更时发送的通知邮件"
file: "notification.html"
subject: "WebSoft订单状态更新"
# 邮件样式配置
styles:
primary_color: "#667eea"
@@ -65,7 +65,7 @@ email:
warning_color: "#ffa502"
danger_color: "#ff6b6b"
info_color: "#4facfe"
# 邮件发送配置
settings:
retry_times: 3

View File

@@ -240,19 +240,18 @@
<!-- Footer -->
<div class="footer">
<div class="footer-content">
<strong>南宁网宿信息科技有限公司</strong><br>
<strong>WebSoft Admin</strong><br>
专业的企业数字化转型服务商
</div>
<div class="footer-links">
<a href="https://www.gxwebsoft.com">官方网站</a>
<a href="https://www.gxwebsoft.com/help">帮助中心</a>
<a href="https://www.gxwebsoft.com/contact">联系我们</a>
<a href="https://websoft.top">官方网站</a>
<a href="https://websoft.top/help">帮助中心</a>
<a href="https://websoft.top/contact">联系我们</a>
</div>
<div class="copyright">
© 2024 南宁网宿信息科技有限公司 版权所有<br>
如有疑问请联系客服170083662@qq.com
© 2025 WebSoft Inc.
</div>
</div>
</div>

View File

@@ -303,7 +303,7 @@
<!-- Footer -->
<div class="footer">
<div class="footer-content">
<strong>网宿软件</strong><br>
<strong>WebSoft Admin</strong><br>
专业的企业数字化转型服务商
</div>

View File

@@ -297,7 +297,7 @@
<!-- CTA Button -->
<div class="cta-section">
<a href="https://www.gxwebsoft.com/login" class="cta-button">立即登录体验</a>
<a href="https://websoft.top/login" class="cta-button">立即登录</a>
</div>
<!-- Features -->
@@ -331,20 +331,19 @@
<!-- Footer -->
<div class="footer">
<div class="footer-content">
<strong>南宁网宿信息科技有限公司</strong><br>
<strong>WebSoft Admin</strong><br>
专业的企业数字化转型服务商
</div>
<div class="footer-links">
<a href="https://www.gxwebsoft.com">官方网站</a>
<a href="https://www.gxwebsoft.com/help">帮助中心</a>
<a href="https://www.gxwebsoft.com/contact">联系我们</a>
<a href="https://websoft.top">官方网站</a>
<a href="https://websoft.top/help">帮助中心</a>
<a href="https://websoft.top/contact">联系我们</a>
</div>
<div class="copyright">
© 2024 南宁网宿信息科技有限公司 版权所有<br>
如有疑问请联系客服170083662@qq.com
</div>
<div class="copyright">
© 2025 WebSoft Inc.
</div>
</div>
</div>
</body>