This commit is contained in:
oneao committed 2026-01-05 17:31:38 +08:00
1 parent f98c280ac4
commit d4b25b13ac
73 files changed
+13112 -1003

No files matched your search

+20
View File
@@ -136,4 +136,24 @@
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<configuration>
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,5 +1,6 @@
package cn.link56.auth.identity.controller;
import cn.link56.auth.identity.model.dto.AuthForgetDTO;
import cn.link56.auth.identity.model.dto.AuthLoginEmailCodeDTO;
import cn.link56.auth.identity.model.dto.AuthLoginPasswordDTO;
import cn.link56.auth.identity.model.dto.AuthRegisterDTO;
@@ -36,6 +37,15 @@ public class AuthIdentityController {
@PostMapping("/login/emailCode")
public Result<AuthLoginVO> loginByEmailCode(@RequestBody @Validated AuthLoginEmailCodeDTO loginEmailCodeDTO){
AuthLoginVO authLoginVO = authIdentityService.loginByEmailCode(loginEmailCodeDTO);
return Result.success(authLoginVO);
}
@PostMapping("/forget")
public Result<Void> loginByEmailCode(@RequestBody @Validated AuthForgetDTO dto){
authIdentityService.forgetPassword(dto.getEmail());
return Result.success();
}
}
@@ -0,0 +1,17 @@
package cn.link56.auth.identity.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthForgetDTO {
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式有误")
private String email;
}
@@ -9,6 +9,7 @@ import cn.link56.auth.identity.model.vo.AuthLoginVO;
import cn.link56.auth.token.model.AuthToken;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.utils.EmailUtils;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.auth.identity.model.dto.AuthRegisterDTO;
@@ -16,11 +17,16 @@ import cn.link56.company.service.CompanyService;
import cn.link56.company.service.CompanyUserService;
import cn.link56.common.exception.BusinessException;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;
@Service
public class AuthIdentityService {
@@ -32,6 +38,8 @@ public class AuthIdentityService {
private AuthTokenService tokenService;
@Resource
private AuthVerificationCodeService authVerificationCodeService;
@Resource
private EmailUtils emailUtils;
@Transactional
public void register(AuthRegisterDTO registerDTO) {
@@ -145,4 +153,18 @@ public class AuthIdentityService {
}
public void forgetPassword(String email) {
CompanyUser companyUser = companyUserService.getByEmail(email);
if (ObjectUtils.isEmpty(companyUser)) {
throw new BusinessException("该邮箱地址未注册,请检查后再试");
}
Map<String,Object> data = new HashMap<String,Object>();
data.put("Password",companyUser.getPassword());
boolean flag = emailUtils.sendSync("ForgetPassword.html", "找回密码", data, email);
System.out.println(flag);
}
}
@@ -1,41 +1,40 @@
package cn.link56.common.config;
import cn.link56.common.interceptor.AuthInterceptor;
import cn.link56.common.interceptor.TokenParsingInterceptor;
import cn.link56.common.interceptor.TokenValidationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
private final TokenParsingInterceptor tokenParsingInterceptor;
private final TokenValidationInterceptor tokenValidationInterceptor;
public WebMvcConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
public WebMvcConfig(TokenParsingInterceptor tokenParsingInterceptor,
TokenValidationInterceptor tokenValidationInterceptor) {
this.tokenParsingInterceptor = tokenParsingInterceptor;
this.tokenValidationInterceptor = tokenValidationInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/**",
"/company/query",
"/company/detail/**",
"/company/recommend",
"/company/recommend/hot",
registry.addInterceptor(tokenParsingInterceptor)
.addPathPatterns("/**");
"/post/tag",
registry.addInterceptor(tokenValidationInterceptor)
.addPathPatterns(
"/im/**",
"/forum/post/page",
"/forum/post/detail",
"/forum/post/getRecommendList",
"/forum/post/getHotList",
"/forum/post/comment/list",
"/company/update",
"/company/user/add",
"/company/user/delete",
"/company/user/update",
"/company/user/query",
"/base/**",
"/error/**",
"/favicon.ico"
"/post/editor/**",
"/post/comment/create",
"/post/comment/delete"
);
}
}
@@ -2,7 +2,6 @@ package cn.link56.common.context;
/**
* 用户信息上下文(ThreadLocal)
* 独立,不依赖 JwtUtils 或拦截器
*/
public class UserContext {
@@ -37,4 +36,8 @@ public class UserContext {
removeUserId();
removeCompanyId();
}
public static boolean isValid() {
return USER_ID.get() != null && COMPANY_ID.get() != null;
}
}
@@ -1,93 +0,0 @@
package cn.link56.common.interceptor;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.Result;
import cn.link56.common.constants.ResponseStatusConstants;
import cn.link56.common.context.UserContext;
import cn.link56.common.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final AuthTokenService authTokenService;
public AuthInterceptor(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 预检请求放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
try {
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
return writeUnauthorized(response);
}
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}else{
return writeUnauthorized(response);
}
boolean flag = authTokenService.validateToken(token);
if (!flag){
return writeUnauthorized(response);
}
// 解析 token
TokenPayload tokenPayload = authTokenService.parsePayload(token);
// 对比信息
String companyIdStr = request.getHeader("X-Company-Id");
String userIdStr = request.getHeader("X-User-Id");
if ((StringUtils.isNotBlank(companyIdStr) && !companyIdStr.equals(tokenPayload.getCompanyId().toString()))
|| (StringUtils.isNotBlank(userIdStr) && !userIdStr.equals(tokenPayload.getUserId().toString()))) {
return writeUnauthorized(response);
}
// 注入到 UserContext
UserContext.setUserId(tokenPayload.getUserId());
UserContext.setCompanyId(tokenPayload.getCompanyId());
return true;
} catch (Exception e) {
return writeUnauthorized(response);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear(); // 清理 ThreadLocal
}
private boolean writeUnauthorized(HttpServletResponse response) throws Exception {
Result<Void> result = Result.error(ResponseStatusConstants.UNAUTHORIZED, "登录过期或未授权");
String jsonString = JsonUtils.toJson(result);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=utf-8");
try (PrintWriter out = response.getWriter()) {
out.println(jsonString);
out.flush();
}
return false;
}
}
@@ -0,0 +1,58 @@
package cn.link56.common.interceptor;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.context.UserContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenParsingInterceptor implements HandlerInterceptor {
private final AuthTokenService authTokenService;
public TokenParsingInterceptor(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 预检请求放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
return true; // 如果没有 token,不做处理,交给下游处理
}
try {
if (token.startsWith("Bearer ")) {
token = token.substring(7); // 去掉 "Bearer " 前缀
} else {
return true; // 如果不是 Bearer token,交给下游处理
}
// 解析 token
TokenPayload tokenPayload = authTokenService.parsePayload(token);
// 将解析后的用户信息存入 UserContext(或其他类似上下文)
UserContext.setUserId(tokenPayload.getUserId());
UserContext.setCompanyId(tokenPayload.getCompanyId());
return true;
} catch (Exception e) {
return true;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear();
}
}
@@ -0,0 +1,51 @@
package cn.link56.common.interceptor;
import cn.link56.auth.token.model.TokenPayload;
import cn.link56.auth.token.service.AuthTokenService;
import cn.link56.common.Result;
import cn.link56.common.constants.ResponseStatusConstants;
import cn.link56.common.context.UserContext;
import cn.link56.common.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Component
public class TokenValidationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean valid = UserContext.isValid(); // 校验是否有效(是否登录)
if (!valid) {
// 用户未登录或信息无效,返回 401 Unauthorized
return writeUnauthorized(response);
}
// 如果有效,允许请求继续
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
private boolean writeUnauthorized(HttpServletResponse response) throws Exception {
Result<Void> result = Result.error(ResponseStatusConstants.UNAUTHORIZED, "登录过期或未授权");
String jsonString = JsonUtils.toJson(result);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=utf-8");
try (PrintWriter out = response.getWriter()) {
out.println(jsonString);
out.flush();
}
return false; // 返回 false,阻止请求继续处理
}
}
@@ -20,6 +20,18 @@ public class CompanyController {
@Resource
private CompanyService companyService;
// 地区
@GetMapping("/area")
public Result<List<AreaVO>> listAreas(){
return Result.success(companyService.listAreas());
}
// 行业
@GetMapping("/industry")
public Result<List<IndustryVO>> listIndustries(){
return Result.success(companyService.listIndustries());
}
// 公司分页
@PostMapping("/query")
public Result<BasePageVO<QueryCompanyVO>> query(@RequestBody QueryCompanyDTO queryCompanyDTO){
@@ -58,13 +70,4 @@ public class CompanyController {
return Result.success();
}
@GetMapping("/area")
public Result<List<AreaVO>> listAreas(){
return Result.success(companyService.listAreas());
}
@GetMapping("/industry")
public Result<List<IndustryVO>> listIndustries(){
return Result.success(companyService.listIndustries());
}
}
@@ -19,6 +19,8 @@ import cn.link56.im.mapper.ImFriendMapper;
import cn.link56.im.mapper.ImGroupMapper;
import cn.link56.im.mapper.ImGroupUserMapper;
import cn.link56.im.storage.SessionManager;
import cn.link56.post.mapper.PostMapper;
import cn.link56.post.service.PostService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -44,6 +46,9 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
@Resource
private CompanyMapper companyMapper;
@Resource
private PostMapper postMapper;
/**
* 判断邮箱是否重复
* @param email 用户邮箱
@@ -74,6 +79,13 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
}
public CompanyUser getByEmail(String email){
LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CompanyUser::getEmail,email);
return companyUserMapper.selectOne(queryWrapper);
}
/**
* 校验当前用户是否有权限操作指定的公司
* @param companyId 当前公司ID
@@ -309,6 +321,8 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
// 执行删除操作
companyUserMapper.delete(updateWrapper);
// 删除该用户的帖子
postMapper.deleteUserPosts(ids,UserContext.getUserId());
}
@@ -44,6 +44,6 @@ public interface ImFriendMapper extends BaseMapper<ImFriend> {
);
@Select("SELECT SUM(b_unread_count) FROM b_im_friend WHERE b_user_id = #{userId}")
@Select("SELECT ISNULL(SUM(b_unread_count), 0) FROM b_im_friend WHERE b_user_id = #{userId}")
Long selectAllUnreadCount(@Param("userId") Long userId);
}
@@ -17,6 +17,6 @@ public interface ImGroupUserMapper extends BaseMapper<ImGroupUser> {
);
@Select("SELECT SUM(b_unread_count) FROM b_im_group_user WHERE b_user_id = #{userId}")
@Select("SELECT ISNULL(SUM(b_unread_count), 0) FROM b_im_group_user WHERE b_user_id = #{userId}")
Long selectAllUnreadCount(@Param("userId") Long userId);
}
@@ -11,9 +11,10 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/editor/post")
@RequestMapping("/post/editor")
public class EditorPostController {
@Resource
private PostService postService;
@@ -43,9 +44,9 @@ public class EditorPostController {
return Result.success();
}
@PostMapping("/delete")
public Result<Void> deletePost(@RequestBody @Validated DeletePostDTO deletePostDTO){
postService.deletePost(deletePostDTO);
@PostMapping("/delete/{postId}")
public Result<Void> deletePost(@PathVariable @NotNull(message = "帖子Id不能为空") Long postId) {
postService.deletePost(postId);
return Result.success();
}
@@ -55,10 +56,4 @@ public class EditorPostController {
return Result.success(data);
}
@GetMapping("/dashboard")
public Result<EditorDashboardVO> getEditorDashboard(){
EditorDashboardVO editorDashboardVO = postService.getEditorDashboard();
return Result.success(editorDashboardVO);
}
}
@@ -40,11 +40,11 @@ public class PostController {
return Result.success(publicPostVO);
}
@GetMapping("/company/{id}")
public Result<PostCompanyVO> getForumCompany(@PathVariable("id") @NotNull(message = "公司ID不能为空") Long companyId){
PostCompanyVO postCompanyVO = postService.getPostCompanyById(companyId);
@GetMapping("/author/{id}")
public Result<PostAuthorProfileVO> getAuthorProfile(@PathVariable("id") @NotNull(message = "文章ID不能为空") Long id){
PostAuthorProfileVO postAuthorProfileVO = postService.getAuthorProfileByPostId(id);
return Result.success(postCompanyVO);
return Result.success(postAuthorProfileVO);
}
}
@@ -1,9 +0,0 @@
package cn.link56.post.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/post/view")
public class PostViewController {
}
@@ -6,6 +6,7 @@ import cn.link56.post.model.vo.PublicPostVO;
import cn.link56.post.model.vo.QueryEditorPostVO;
import cn.link56.post.model.vo.QueryPublicPostVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@@ -32,10 +33,6 @@ public interface PostMapper extends BaseMapper<Post> {
@Param("month") String month
);
EditorDashboardVO.Count selectDashboardOverview(@Param("userId") Long userId);
List<EditorDashboardVO.Chart> selectDashboardViewStats(@Param("userId") Long userId);
List<EditorDashboardVO.Chart> selectDashboardNumberStats(@Param("userId") Long userId);
List<QueryPublicPostVO> selectPublicPostPage(
@Param("offset") int offset,
@@ -51,4 +48,6 @@ public interface PostMapper extends BaseMapper<Post> {
@Update("UPDATE b_post SET b_active_time = GETDATE() WHERE b_id = #{postId}")
int updateActiveTime(@Param("postId") Long postId);
void deleteUserPosts(@Param("userIds") List<Long> userIds,Long updateBy);
}
@@ -0,0 +1,17 @@
package cn.link56.post.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreatePostViewDTO {
@NotNull(message = "帖子ID不能为空")
private Long postId;
private Long userId;
}
@@ -1,9 +1,6 @@
package cn.link56.post.model.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -64,4 +61,11 @@ public class Post {
value = "b_active_time"
)
private Date activeTime;
@TableLogic
@TableField(
value = "b_deleted",
fill = FieldFill.INSERT
)
private Integer deleted;
}
@@ -8,6 +8,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -26,5 +28,5 @@ public class PostStat {
value = "b_update_time",
fill = FieldFill.UPDATE
)
private Long createTime;
private Date createTime;
}
@@ -8,6 +8,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -26,5 +28,5 @@ public class PostView {
value = "b_create_time",
fill = FieldFill.INSERT
)
private Long createTime;
private Date createTime;
}
@@ -0,0 +1,40 @@
package cn.link56.post.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostAuthorProfileVO {
private Company company;
private User user;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class User{
private Long id;
private String avatar;
private String name;
private String position;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Company {
private Long id;
private String name;
private String logo;
private String profile;
private Date establishmentDate;
private List<String> areas;
private List<String> industries;
}
}
@@ -1,21 +0,0 @@
package cn.link56.post.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostCompanyVO {
private Long id;
private String name;
private String logo;
private String profile;
private Date establishmentDate;
private List<String> areas;
private List<String> industries;
}
@@ -17,8 +17,8 @@ public class PublicPostVO {
private List<String> tags;
private Date activeTime;
private Long viewCount;
private Long commentCount;
private Integer viewCount;
private Integer commentCount;
private Long companyId;
}
@@ -22,7 +22,8 @@ public class QueryEditorPostVO {
private Integer status;
private Integer viewCount;
private Integer likeCount;
private Integer favoriteCount;
private Integer commentCount;
private Long editorId;
private String editorName;
}
@@ -4,13 +4,16 @@ import cn.link56.common.BasePageVO;
import cn.link56.common.context.UserContext;
import cn.link56.common.exception.BusinessException;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.company.mapper.CompanyMapper;
import cn.link56.company.mapper.CompanyUserMapper;
import cn.link56.company.model.po.Company;
import cn.link56.company.model.po.CompanyUser;
import cn.link56.company.service.CompanyService;
import cn.link56.company.service.CompanyUserService;
import cn.link56.post.mapper.PostMapper;
import cn.link56.post.model.dto.*;
import cn.link56.post.model.enums.PostStatusEnum;
import cn.link56.post.model.po.Post;
import cn.link56.post.model.po.PostComment;
import cn.link56.post.model.vo.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -34,11 +37,11 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
@Resource
private PostViewService postViewService;
@Resource
private PostCommentService postCommentService;
@Resource
private CompanyService companyService;
@Resource
private PostStatService postStatService;
@Resource
private CompanyMapper companyMapper;
@Resource
private CompanyUserMapper companyUserMapper;
@Transactional
public EditorPostVO createPost(CreatePostDTO dto) {
@@ -145,17 +148,9 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
}
@Transactional
public void deletePost(DeletePostDTO deletePostDTO) {
Long postId = deletePostDTO.getId();
public void deletePost(Long postId) {
// 删除帖子
postMapper.deleteById(postId);
// 删除帖子统计
postStatService.removeById(postId);
// 删除帖子浏览
postViewService.deleteByPostId(postId);
// 删除帖子评论
postCommentService.deleteByPostId(postId);
}
@Transactional
@@ -172,27 +167,6 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
postMapper.updateById(post);
}
public EditorDashboardVO getEditorDashboard() {
Long userId = UserContext.getUserId();
EditorDashboardVO editorDashboardVO = new EditorDashboardVO();
EditorDashboardVO.Count count = postMapper.selectDashboardOverview(userId);
// 原始数据
List<EditorDashboardVO.Chart> viewCharts = postMapper.selectDashboardViewStats(userId);
List<EditorDashboardVO.Chart> numberCharts = postMapper.selectDashboardNumberStats(userId);
List<EditorDashboardVO.Chart> typeCharts = getDashboardTypeStats(userId);
// 统一处理最近 7 天,补齐缺失值
editorDashboardVO.setViewChart(fillLast7Days(viewCharts));
editorDashboardVO.setNumberChart(fillLast7Days(numberCharts));
editorDashboardVO.setTypeChart(typeCharts);
editorDashboardVO.setAllCount(count);
return editorDashboardVO;
}
/**
* 填充最近7天的 chart 数据,如果 map 里没有,则补 0
*/
@@ -275,27 +249,46 @@ public class PostService extends ServiceImpl<PostMapper, Post> {
BeanUtils.copyProperties(post,postVO);
postVO.setTags(Arrays.stream(StringUtils.split(post.getTag(),",")).collect(Collectors.toList()));
Long viewTotal = postViewService.getTotalByPostId(id);
Long commentTotal = postCommentService.getTotalByPostId(id);
Integer viewTotal = postStatService.getPostViewCount(id);
Integer commentTotal = postStatService.getPostCommentCount(id);
postVO.setViewCount(viewTotal);
postVO.setCommentCount(commentTotal);
// 新增view数量
postViewService.create(id,UserContext.getUserId());
return postVO;
}
public PostCompanyVO getPostCompanyById(Long id) {
Company company = companyService.getById(id);
public PostAuthorProfileVO getAuthorProfileByPostId(Long id) {
Post post = postMapper.selectById(id);
Company company = companyMapper.selectById(post.getCompanyId());
if (company == null){
throw new BusinessException("该公司不存在或已注销");
}
PostCompanyVO postCompanyVO = new PostCompanyVO();
BeanUtils.copyProperties(company,postCompanyVO);
// company
PostAuthorProfileVO.Company companyVO = new PostAuthorProfileVO.Company();
postCompanyVO.setAreas(Arrays.stream(StringUtils.split(company.getArea(),",")).collect(Collectors.toList()));
postCompanyVO.setIndustries(Arrays.stream(StringUtils.split(company.getIndustry(),",")).collect(Collectors.toList()));
BeanUtils.copyProperties(company, companyVO);
return postCompanyVO;
companyVO.setAreas(Arrays.stream(StringUtils.split(company.getArea(),",")).collect(Collectors.toList()));
companyVO.setIndustries(Arrays.stream(StringUtils.split(company.getIndustry(),",")).collect(Collectors.toList()));
// user
CompanyUser companyUser = companyUserMapper.selectById(post.getCreateBy());
PostAuthorProfileVO.User userVO =new PostAuthorProfileVO.User();
BeanUtils.copyProperties(companyUser,userVO);
PostAuthorProfileVO postAuthorProfileVO = new PostAuthorProfileVO();
postAuthorProfileVO.setCompany(companyVO);
postAuthorProfileVO.setUser(userVO);
return postAuthorProfileVO;
}
}
@@ -2,9 +2,8 @@ package cn.link56.post.service;
import cn.link56.post.mapper.PostStatMapper;
import cn.link56.post.model.po.PostStat;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -39,4 +38,24 @@ public class PostStatService extends ServiceImpl<PostStatMapper, PostStat> {
this.save(stat);
}
public Integer getPostViewCount(Long postId){
PostStat postStat = postStatMapper.selectById(postId);
if (ObjectUtils.isEmpty(postStat)){
return 0;
}
return postStat.getViewCount();
}
public Integer getPostCommentCount(Long postId){
PostStat postStat = postStatMapper.selectById(postId);
if (ObjectUtils.isEmpty(postStat)){
return 0;
}
return postStat.getCommentCount();
}
}
@@ -1,9 +1,13 @@
package cn.link56.post.service;
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.post.mapper.PostViewMapper;
import cn.link56.post.model.dto.CreatePostViewDTO;
import cn.link56.post.model.po.PostView;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@@ -11,7 +15,8 @@ import javax.annotation.Resource;
public class PostViewService extends ServiceImpl<PostViewMapper, PostView> {
@Resource
private PostViewMapper postViewMapper;
@Resource
private PostStatService postStatService;
/**
* 根据文章 id 获取浏览总数
@@ -30,4 +35,17 @@ public class PostViewService extends ServiceImpl<PostViewMapper, PostView> {
.eq(PostView::getPostId, postId)
.remove();
}
@Transactional
public void create(Long postId,Long userId) {
if (ObjectUtils.isNotEmpty(userId)){
PostView postView = new PostView();
postView.setId(IdGenerator.nextId());
postView.setPostId(postId);
postView.setUserId(userId);
postViewMapper.insert(postView);
}
postStatService.increaseViewCount(postId);
}
}
@@ -0,0 +1,58 @@
server:
port: 8090
netty:
port: 8080
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: net.sourceforge.jtds.jdbc.Driver
url: jdbc:jtds:sqlserver://127.0.0.1:1433;databaseName=G3-Link56
username: sa
password: "123456@aaa"
# druid 连接池管理
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池的最小空闲数量
min-idle: 5
# 连接池最大连接数量
max-active: 20
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
# 既作为检测的间隔时间又作为testWhileIdel执行的依据
time-between-eviction-runs-millis: 60000
# 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间)
min-evictable-idle-time-millis: 30000
# 用来检测数据库连接是否有效的sql 必须是一个查询语句(oracle中为 select 1 from dual) validation-query: select 'x'
# 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false
# 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false
# 是否缓存preparedStatement, 也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。
pool-prepared-statements: false
# 置监控统计拦截的filters,去掉后监控界面sql无法统计,stat: 监控统计、Slf4j:日志记录、waLL: 防御sqL注入
filters: stat,wall,slf4j
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
max-pool-prepared-statement-per-connection-size: -1
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
stat-view-servlet:
# 是否启用StatViewServlet默认值true
enabled: true
# 访问路径为/druid时,跳转到StatViewServlet
url-pattern: /druid/*
# 是否能够重置数据
reset-enable: false
# 需要账号密码才能访问控制台,默认为root
login-username: root
login-password: root
# IP白名单
allow: 127.0.0.1
# IP黑名单(共同存在时,deny优先于allow)
deny:
@@ -15,21 +15,19 @@ logging:
spring:
profiles:
active: dev
active: prod
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mail:
host: smtp.163.com #邮箱服务器地址
username: hi_itao@163.com #邮箱账号
password: GVdEzKAee9MByXdy #邮箱授权码
password: RSchPDWrKwVcyMG7 #邮箱授权码
default-encoding: utf-8 #默认编码
mybatis-plus:
# 对应的 XML 文件位置
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: cn.link56.domain
# configLocation: classpath:mybatis/mybatis-config.xml
configuration:
cache-enabled: true
@@ -72,7 +72,7 @@
</if>
<if test="title != null and title != ''">
AND p.b_title LIKE CONCAT('%', #{title}, '%')
AND p.b_title LIKE '%' + #{title} + '%'
</if>
<if test="year != null and year != ''">
@@ -82,6 +82,8 @@
<if test="month != null and month != ''">
AND MONTH(p.b_update_time) = #{month}
</if>
AND p.b_deleted = 0
</where>
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
@@ -111,47 +113,10 @@
<if test="month != null and month != ''">
AND MONTH(p.b_update_time) = #{month}
</if>
AND p.b_deleted = 0
</where>
</select>
<select id="selectDashboardOverview"
resultType="cn.link56.post.model.vo.EditorDashboardVO$Count">
SELECT
COUNT(p.b_id) AS postCount,
ISNULL(SUM(ps.b_view_count), 0) AS viewCount,
ISNULL(SUM(ps.b_like_count), 0) AS likeCount
FROM b_post p
LEFT JOIN b_post_stat ps ON ps.b_post_id = p.b_id
WHERE p.b_create_by = #{userId}
</select>
<select id="selectDashboardViewStats" resultType="cn.link56.post.model.vo.EditorDashboardVO$Chart">
SELECT
CONVERT(VARCHAR(10), v.b_create_time, 120) AS label,
COUNT(*) AS value
FROM b_post_view v
JOIN b_post p ON v.b_post_id = p.b_id
WHERE p.b_create_by = #{userId}
AND v.b_create_time &gt;= CONVERT(DATE, DATEADD(DAY, -6, GETDATE()))
AND v.b_create_time &lt; CONVERT(DATE, DATEADD(DAY, 1, GETDATE()))
GROUP BY CONVERT(VARCHAR(10), v.b_create_time, 120)
ORDER BY label
</select>
<select id="selectDashboardNumberStats" resultType="cn.link56.post.model.vo.EditorDashboardVO$Chart">
SELECT
CONVERT(VARCHAR(10), p.b_create_time, 120) AS label,
COUNT(*) AS value
FROM b_post p
WHERE p.b_create_by = #{userId}
AND p.b_create_time &gt;= CONVERT(DATE, DATEADD(DAY, -6, GETDATE()))
AND p.b_create_time &lt; CONVERT(DATE, DATEADD(DAY, 1, GETDATE()))
GROUP BY CONVERT(VARCHAR(10), p.b_create_time, 120)
ORDER BY label
</select>
<select id="selectPublicPostPage" resultMap="QueryPublicPostVOMap">
SELECT
t.id,
@@ -191,6 +156,7 @@
<if test="tag != null and tag != ''">
AND p.b_tag LIKE '%' + #{tag} + '%'
</if>
AND p.b_deleted = 0
</where>
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
@@ -209,7 +175,22 @@
<if test="tag != null and tag != ''">
AND p.b_tag LIKE '%' + #{tag} + '%'
</if>
AND p.b_deleted = 0
</where>
</select>
<!-- 批量软删除用户的帖子 -->
<delete id="deleteUserPosts">
UPDATE b_post
SET b_deleted = 1,
update_by = #{updateBy},
update_time = GETDATE()
WHERE b_create_by IN
<foreach item="userId" collection="userIds" open="(" separator="," close=")">
#{userId}
</foreach>
AND b_deleted = 0
</delete>
</mapper>
@@ -11,7 +11,6 @@
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
@@ -381,6 +380,7 @@
width: 100%;
margin: 30px auto;
padding: 0;
font-size: 42px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
@@ -464,7 +464,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
@@ -478,29 +478,17 @@
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>您好 [[${Name}]],</h1>
<p>您最近请求重置 link56 账户的密码。请点击下面的按钮进行密码重置。<strong>该重置链接仅在 10 分钟内有效。</strong></p>
<h1>您好</h1>
<p>
您正在 <strong>link56</strong> 平台进行<strong>找回密码</strong>操作,
密码如下,请注意保存。
</p>
<!-- Action -->
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0"
role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="[[${ResetUrl}]]" class="f-fallback button button--green"
target="_blank">重置您的密码</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p class="body-action">
[[${Password}]]
</p>
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
href="[[${SupportUrl}]]">联系客服</a>。</p>
href="{{SupportUrl}}">联系客服</a>。</p>
<p>
感谢,<br>
@@ -0,0 +1,524 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
font-size: 16px;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.u-margin-bottom-none {
margin-bottom: 0;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
border-right: 18px solid #FF6136;
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
font-size: 42px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3,
span,
.purchase_item {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="http://link56.net" class="f-fallback email-masthead_name">
link56
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>您好</h1>
<p>
您正在 <strong>link56</strong> 平台进行<strong>找回密码</strong>操作,
密码如下,请注意保存。
</p>
<p class="body-action">
[[${Password}]]
</p>
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
href="{{SupportUrl}}">联系客服</a>。</p>
<p>
感谢,<br>
link56 团队
</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">
宁波江东远通计算机有限公司
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -11,7 +11,6 @@
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
@@ -465,7 +464,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
@@ -465,7 +465,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>
@@ -465,7 +465,7 @@
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead_name">
<a href="http://link56.net/" class="f-fallback email-masthead_name">
link56
</a>
</td>