2026/3/2 2:17:25
网站建设
项目流程
长沙企业网站推广服务公司,外包做网站需要多少钱,我被钓鱼网站骗了骗取建设信用卡建设银行会怎么处理钱会还回吗,软件优化网站前言#xff1a;
该文档只作为本人学习过程的记录#xff0c;若还需要更详细的项目文档可以点击下方链接进行购买
文档地址
同时该项目已经在git上面开源#xff0c;可以在购买前去看一下该项目。
项目后端的git地址#xff1a;知光git后端地址
项目前端的git地址: 知…前言该文档只作为本人学习过程的记录若还需要更详细的项目文档可以点击下方链接进行购买文档地址同时该项目已经在git上面开源可以在购买前去看一下该项目。项目后端的git地址知光git后端地址项目前端的git地址: 知光git前端地址由于本人还在开发学习当中如果要看本人的代码可以进入以下地址本人的gitee地址1 对象存储模块的实现1.1 准备对象存储模块数据传输需要的DTO,VOpackage com.xiaoce.zhiguang.Oss.domain.dto; import jakarta.validation.constraints.NotBlank; /** * StoragePresignRequest * p * 对象存储dto * 作用前端手机/网页想传文件时必须先填好这张“申请表”发给后端。 * 后端根据这张表的内容向阿里云/腾讯云申请一个“临时上传通行证”。 * * author 小策 * date 2026/1/20 13:26 */ public record StoragePresignRequest( /** * 业务场景 (必填) * 作用告诉后端这个文件是干嘛的决定文件存到哪个文件夹。 * 例如 * - knowpost_avatar: 用户头像 - 存入 /avatars 目录 * - knowpost_image: 文章配图 - 存入 /posts 目录 */ NotBlank(message 业务场景不能为空) String scene, /** * 关联的帖子ID / 业务ID (必填) * 作用标记这张图属于哪篇文章或哪个用户。 * ⚠️ 重点使用 String 类型而不是 Long是为了防止前端浏览器处理超长数字时丢失精度变乱码。 */ NotBlank(message 业务ID不能为空) String postId, /** * 文件内容类型 (必填) * 作用声明你要传什么格式的文件用于安全检查。 * 例如 * - image/jpeg (jpg图片) * - image/png (png图片) * OSS 会校验实际上传的文件是否匹配这个类型防止恶意文件混入。 */ NotBlank(message 文件类型不能为空) String contentType, /** * 文件后缀名 (选填) * 作用文件的扩展名用于生成最终的文件名。 * 例如 jpg, png, mp4 */ String ext ) { // record 类不需要写 getter/setter/toStringJava 编译器会自动生成 }package com.xiaoce.zhiguang.Oss.domain.vo; import java.util.Map; /** * StoragePresignResponse * p * 对象存储VO * 作用这是后端“管家”审核完“申请表”后开具的“临时通行证”。 * 前端拿到它就有了直接往云仓库OSS塞东西的权限。 * author 小策 * date 2026/1/20 13:30 */ public record StoragePresignResponse( /** * 文件在云端的存储 Key (门牌号) * 作用文件上传成功后它在云仓库里的唯一名字。 * 例如posts/2023/10/01/uuid_image.jpg * ⚠️ 重要前端上传完成后需要把这个 Key 存到数据库里以后就靠它找图片。 */ String objectKey, /** * 预签名上传地址 (专用通道) * 作用这是一个带了“加密签名”的长链接。 * 前端不需要知道账号密码直接往这个 URL 发送 PUT 请求就能把文件传上去。 */ String putUrl, /** * 必须携带的请求头 (暗号) * 作用OSS 厂商有时要求上传时必须带上特定的 Header。 * 例如{Content-Type: image/png} * 前端在发起 HTTP 请求时要把这些键值对放到 Header 里否则 OSS 会拒收。 */ MapString, String headers, /** * 通行证有效期 (秒) * 作用这个 URL 不是永久有效的。 * 比如 300 (5分钟)。如果用户在页面上发呆 10 分钟才点上传这个链接就失效了需要重新申请。 */ int expiresIn ) { }1.2 准备读取OSS配置的属性类package com.xiaoce.zhiguang.Oss.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * OssProperties * p * 对象存储的属性配置类 * * author 小策 * date 2026/1/20 13:00 */ Component Data ConfigurationProperties(prefix oss) public class OssProperties { /** * OSS 的地址 (Endpoint) */ private String endpoint; /** * AccessKey ID (公钥) * 对应 yaml: access-key-id */ private String accessKeyId; /** * AccessKey Secret (私钥) * 对应 yaml: access-key-secret */ private String accessKeySecret; /** * 存储桶名称 * 对应 yaml: bucket-name */ private String bucketName; /** * 自定义域名 / CDN 加速地址 * 对应 yaml: public-domain */ private String publicDomain; /** * 文件存储目录 * 对应 yaml: folder */ private String folder avatars; }1.3 生成知文相关的po,controller,service,mapper等那么问题来了,为什么对象存储模块需要使用知文模块的相关信息这是一个非常深刻的架构问题通常我们说“高内聚低耦合”让对象存储模块Storage去依赖知文模块KnowPost看起来好像有点“耦合”了。但在这里这种依赖是必须的核心原因有两个“防越权安全”和“精准归档业务”。我们可以把对象存储模块比作“仓库管理员”把知文模块比作“房屋租赁中心”。核心原因一安全校验防止越权这是最关键的原因。如果不查知文模块的信息就会出现严重的安全漏洞。场景假设坏人User A想搞破坏他想往好人User B的文章里上传一张违规图片或者把 User B 的文章配图覆盖掉。坏人向你的接口发送请求postId User_B_Post_ID。如果不查知文模块存储模块只管开门看到请求里有postId就生成了上传链接。结果坏人拿到了 User B 文件夹的上传权限成功篡改了数据。现在的逻辑查知文模块代码if (!post.getCreatorId().equals(userId))仓库管理员存储模块拿着postId打电话给租赁中心知文模块“喂查一下 ID 为 10086 的这个帖子它的房主是谁”租赁中心“房主是 User B。”仓库管理员一看“那不行现在的申请人是 User A拒绝开门”结论只有知文模块知道“这个帖子属于谁”。存储模块必须获取这个信息才能判断当前操作者是否有权限上传。核心原因二构建规范的存储路径精准归档OSS 上的文件不能乱放需要按照业务结构整理。代码逻辑JavaobjectKey posts/ postId /content ext;依赖点我们需要确保这个postId是真实存在的。如果用户传了一个数据库里根本不存在的postId 999999如果不校验直接上传OSS 里就会产生一堆垃圾文件夹posts/999999/...这些文件永远不会被用到浪费钱还难清理。核心原因三兜底业务规则有时候业务会有特殊规定比如“只有草稿状态Draft的文章允许上传图片已发布Published的文章不允许修改。”“只有 VIP 用户的文章允许上传视频。”这些状态信息Status和用户等级信息全部存在知文模块的数据库里。存储模块必须把帖子信息查出来KnowPosts post ...才能执行这些逻辑判断。架构设计的思考给小白的进阶你可能会问“那这样存储模块岂不是和知文模块绑死了如果以后拆分微服务怎么办”你说得对这确实增加了耦合。在大型微服务架构中通常有几种解法现有方案单体/小微服务直接注入 Mapper/Service 查询。优点是开发快、性能高、事务好控制。推荐你现在的做法RPC调用大型微服务存储服务调用知文服务的 Feign 接口HTTP请求去查询。原理一样只是代码形式变了。Token 携带无状态前端在请求时Token 里不仅带 UserID还带上某种加密的“资源操作令牌”。实现极其复杂一般不推荐。总结这里“跨模块”查询是为了确认“你是你你的帖子是你的帖子”。在涉及数据修改上传文件的操作中安全校验的优先级高于解耦。1.3.1 填写好生成代码的位置生成的po代码展示package com.xiaoce.zhiguang.KnowPost.domain.po; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.io.Serializable; import java.time.LocalDateTime; /** * p * 知识库文章表 * /p * * author 小策 * since 2026-01-20 */ Data EqualsAndHashCode(callSuper false) Accessors(chain true) TableName(know_posts) Builder Schema(description 知识库文章实体对象) public class KnowPosts implements Serializable { private static final long serialVersionUID 1L; Schema(description 主键ID (雪花算法生成), example 1745678901234567890) TableId(value id, type IdType.ASSIGN_ID) private Long id; Schema(description 作者ID关联 users.id, example 1745678901234567800) private Long creatorId; Schema(description 主分类ID, example 101) private Long tagId; Schema(description 标签数组 JSON, example [\Java\, \Spring\]) private String tags; Schema(description 标题, example 深入理解 Java 虚拟机) private String title; Schema(description 摘要, example 本文详细介绍了 JVM 的内存模型...) private String description; Schema(description 文章内容OSS地址 (大字段分离), example https://oss.zhiguang.com/posts/content/1.md) private String contentUrl; Schema(description OSS Key, example posts/content/1.md) private String contentObjectKey; Schema(description OSS ETag校验) private String contentEtag; Schema(description 正文大小 (字节)) private Long contentSize; Schema(description 是否置顶 (0:否, 1:是), example false) private Boolean isTop; Schema(description 类型 (image, text, video), example text) private String type; Schema(description 状态: draft(草稿), auditing(审核中), published(已发布), example published) private String status; Schema(description 可见性 (public/private), example public) private String visible; Schema(description 图片列表 JSON, example [\https://oss.../1.jpg\]) private String imgUrls; Schema(description 视频地址) private String videoUrl; Schema(description 创建时间) private LocalDateTime createTime; Schema(description 更新时间) private LocalDateTime updateTime; Schema(description 发布时间) private LocalDateTime publishTime; }1.4 对象存储模块的接口文档接口详情获取OSS直传预签名URL用于前端在上传大文件如文章配图、Markdown内容之前获取阿里云/腾讯云 OSS 的临时上传链接。前端拿到链接后直接使用 PUT 方法上传文件无需经过后端服务器。URL:/api/v1/storage/presignMethod:POST权限:需要认证 (Header 需携带 Authorization: Bearer {Token})请求参数 (Body):scene(String, 必填): 业务场景可选值knowpost_content(文章内容),knowpost_image(文章配图)。postId(String, 必填): 关联的帖子ID/业务ID (使用字符串类型防止精度丢失)。contentType(String, 必填): 文件的 MIME 类型例如image/jpeg,text/markdown。ext(String, 选填): 文件后缀名例如.jpg,.md(若不填后端会自动推断)。响应示例 (200 OK):JSON{ objectKey: posts/1745678901234567890/images/20260120/a1b2c3d4.jpg, putUrl: https://your-bucket.oss-cn-beijing.aliyuncs.com/posts/...?Expires1705SignatureXxYyZz..., headers: { Content-Type: image/jpeg }, expiresIn: 600 }1.5 对象存储模块controller层实现package com.xiaoce.zhiguang.Oss.controller; import com.xiaoce.zhiguang.Oss.domain.dto.StoragePresignRequest; import com.xiaoce.zhiguang.Oss.domain.vo.StoragePresignResponse; import com.xiaoce.zhiguang.Oss.service.IOssStorageService; import com.xiaoce.zhiguang.auth.service.impl.JwtServiceImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.validation.annotation.Validated; 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; /** * OssStorageController * p * 对象存储的controller * * author 小策 * date 2026/1/20 13:50 */ RequestMapping(/api/v1/storage) RestController Validated RequiredArgsConstructor Tag(name 对象存储) public class OssStorageController { private final IOssStorageService ossStorageService; private final JwtServiceImpl jwtService; /** * 获取用于直传的 PUT 预签名 URL。 */ PostMapping(/presign) Operation( summary 获取OSS直传预签名URL, // 2. 简短的标题 description 前端在上传大文件如文章图片、Markdown内容之前必须先调用此接口。 接口会校验用户权限并返回一个阿里云OSS的临时上传链接PUT方法。 前端拿到链接后请直接使用 HTTP PUT 方法将文件二进制流上传到该地址。 // 3. 详细的说明书 ) ApiResponses({ ApiResponse( responseCode 200, description 获取预签名URL成功, content Content(schema Schema(implementation StoragePresignResponse.class)) ), ApiResponse(responseCode 400, description 获取预签名URL失败) }) public StoragePresignResponse presign(Valid RequestBody StoragePresignRequest request, AuthenticationPrincipal Jwt jwt) { // 1. 既然是直传必须知道是谁传的 long userId jwtService.extractUserId(jwt); // 2. 直接调用 Service 的高层业务方法 return ossStorageService.createPresign(request, userId); } }1.6 对象存储模块IOssStorageService接口实现package com.xiaoce.zhiguang.Oss.service; import com.xiaoce.zhiguang.Oss.domain.dto.StoragePresignRequest; import com.xiaoce.zhiguang.Oss.domain.vo.StoragePresignResponse; import jakarta.validation.Valid; /** * IOssStorageService * p * “开临时通行证”让前端直传和 “亲自搬运”帮后端上传头像的接口 * * author 小策 * date 2026/1/20 13:46 */ public interface IOssStorageService { StoragePresignResponse createPresign(Valid StoragePresignRequest request, long userId); }1.7 对象存储模块OssStorageServiceImpl实现1.7.1 实现逻辑1.7.1.1 预签名代码实现逻辑1.7.1.2 上传or更新头像实现逻辑1.7.2 实现代码package com.xiaoce.zhiguang.Oss.service.Impl; import com.aliyun.oss.HttpMethod; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.GeneratePresignedUrlRequest; import com.aliyun.oss.model.PutObjectRequest; import com.xiaoce.zhiguang.KnowPost.domain.po.KnowPosts; import com.xiaoce.zhiguang.KnowPost.mapper.KnowPostsMapper; import com.xiaoce.zhiguang.Oss.config.OssProperties; import com.xiaoce.zhiguang.Oss.domain.dto.StoragePresignRequest; import com.xiaoce.zhiguang.Oss.domain.vo.StoragePresignResponse; import com.xiaoce.zhiguang.Oss.service.IOssStorageService; import com.xiaoce.zhiguang.common.exception.BusinessException; import com.xiaoce.zhiguang.common.exception.ErrorCode; import com.xiaoce.zhiguang.common.utils.FileUtil; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.net.URL; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Map; import java.util.UUID; /** * OssStorageServiceImpl * p *“开临时通行证”让前端直传和 “亲自搬运”帮后端上传头像的实现 * * author 小策 * date 2026/1/20 13:47 */ Service RequiredArgsConstructor public class OssStorageServiceImpl implements IOssStorageService { private final OssProperties props; private final KnowPostsMapper knowPostMapper; /** * 生成用于上传文件PUT 请求的预签名 URL。 * p * 该接口用于授权前端或客户端在指定时间内直接将文件上传到对象存储服务如 AWS S3、阿里云 OSS、MinIO * 而无需通过后端服务器中转文件流从而减轻后端服务器压力。 * p * strong注意/strong 前端在执行 PUT 请求时必须设置 Header 中的 Content-Type 与此处传入的一致否则会报签名错误。 * param objectKey 对象在存储桶中的唯一标识即文件路径文件名例如 avatar/user_1001.jpg。 * param contentType 文件的 MIME 类型例如 image/jpeg、application/pdf。 * br该字段不能为空用于确保上传的文件类型符合预期。 * param expiresInSeconds URL 的有效时长单位秒。超过该时间后 URL 将失效。 * return 包含签名信息的完整上传 URL 字符串。客户端可直接向此 URL 发起 PUT 请求上传文件。 */ public String generatePresignedPutUrl(String objectKey, NotBlank(message 文件类型不能为空) String contentType, int expiresInSeconds) { ensureConfigured(); // 确保配置已设置 OSS client new OSSClientBuilder().build(props.getEndpoint(), props.getAccessKeyId(), props.getAccessKeySecret()); try { // 创建过期时间对象将当前时间加上指定的秒数转换为毫秒 Date expiration new Date(System.currentTimeMillis() expiresInSeconds * 1000L); // 创建生成预签名URL请求对象指定存储桶名称、对象键和HTTP方法为PUT GeneratePresignedUrlRequest request new GeneratePresignedUrlRequest(props.getBucketName(), objectKey, HttpMethod.PUT); // 设置URL的过期时间 request.setExpiration(expiration); // 如果内容类型不为空且不是空白字符串则设置请求的内容类型 if (contentType ! null !contentType.isBlank()) { request.setContentType(contentType); } // 生成预签名URL并将其转换为字符串返回 URL url client.generatePresignedUrl(request); return url.toString(); } finally { // 在代码块执行完毕后确保关闭客户端资源 client.shutdown(); } } /** * 生成对象的公共访问URL * param objectKey 对象的键文件名/路径 * return 返回可公开访问的URL字符串 */ private String publicUrl(String objectKey) { // 检查是否配置了自定义的公共域名 if (props.getPublicDomain() ! null !props.getPublicDomain().isBlank()) { // 如果配置了公共域名则使用自定义域名 // 去除域名末尾的斜杠然后拼接对象键 return props.getPublicDomain().replaceAll(/$, ) / objectKey; } // 如果未配置自定义域名则使用默认的S3风格URL // 格式为https://桶名.端点/对象键 return https:// props.getBucketName() . props.getEndpoint() / objectKey; } /** * 确保对象存储配置已正确设置 * 检查必要的配置参数是否已设置包括端点、访问密钥ID、访问密钥密钥和存储桶名称 * 如果任何必需的配置参数缺失将抛出业务异常 * * throws BusinessException 当任何必需的配置参数缺失时抛出错误码为BAD_REQUEST消息为对象存储未配置 */ private void ensureConfigured() { // 检查端点、访问密钥ID、访问密钥密钥和存储桶名称是否都已配置 if (props.getEndpoint() null || props.getAccessKeyId() null || props.getAccessKeySecret() null || props.getBucketName() null) { // 如果任何必需的配置参数为null抛出业务异常 throw new BusinessException(ErrorCode.BAD_REQUEST, 对象存储未配置); } } /** * 生成预签名URL * param request 包含生成预签名URL所需的信息 * param userId 当前用户的ID * return 包含预签名URL和对象键的响应对象 */ Override public StoragePresignResponse createPresign(StoragePresignRequest request, long userId) { // 声明一个长整型变量postId用于存储帖子ID long postId; try { // 尝试将请求中的postId字符串转换为长整型 postId Long.parseLong(request.postId()); } catch (NumberFormatException e) { // 如果转换失败抛出业务异常提示postId非法 throw new BusinessException(ErrorCode.BAD_REQUEST, postId 非法); } // 根据postId从数据库查询帖子信息 KnowPosts post knowPostMapper.selectById(postId); // 检查帖子是否存在、创建者ID是否为空、当前用户是否为帖子创建者 // 如果检查不通过抛出业务异常提示草稿不存在或无权限 if(post null || post.getCreatorId() null || !post.getCreatorId().equals(userId)){ throw new BusinessException(ErrorCode.BAD_REQUEST, 草稿不存在或无权限); } // 3. 生成 ObjectKey // 获取场景 String scene request.scene(); String objectKey; //文件上传成功后它在云仓库里的唯一名字。 //得到文件后缀 String ext FileUtil.normalizeExt(request.ext(), request.contentType(), scene); // 判断上传场景是否为knowpost_content if (knowpost_content.equals(scene)) { // 如果是内容场景则构建内容存储的objectKey格式为posts/{postId}/content{文件扩展名} objectKey posts/ postId /content ext; } else if (knowpost_image.equals(scene)) { //如果是文章内的图片 (knowpost_image),因此这里的objectKey会和下述的上传头像的objectKey不同 // 使用当前UTC日期格式化为yyyyMMdd格式的字符串 String date DateTimeFormatter.ofPattern(yyyyMMdd).withZone(ZoneId.of(UTC)).format(Instant.now()); // 生成一个UUID移除其中的连字符并取前8个字符作为随机字符串 String rand UUID.randomUUID().toString().replaceAll(-, ).substring(0, 8); objectKey posts/ postId /images/ date / rand ext; } else { throw new BusinessException(ErrorCode.BAD_REQUEST, 不支持的上传场景); } // 4. 生成签名URL // 设置过期时间为600秒即10分钟 int expiresIn 600; // 10 分钟 // 生成预签名上传URL包含对象键、内容类型和过期时间 String putUrl this.generatePresignedPutUrl(objectKey, request.contentType(), expiresIn); // 创建请求头映射设置内容类型 MapString, String headers Map.of(Content-Type, request.contentType()); return new StoragePresignResponse(objectKey, putUrl, headers, expiresIn); } /** * 更新用户头像方法 * param userId 用户ID * param file 用户上传的头像文件 * return 返回头像的公开访问URL */ public String updateAvatar(Long userId , MultipartFile file) { // 确保配置已正确设置 ensureConfigured(); // 获取原始文件名 String originalFilename file.getOriginalFilename(); String ext ; // 如果文件名不为空且包含点号则获取文件扩展名 if (originalFilename ! null originalFilename.contains(.)) { ext originalFilename.substring(originalFilename.lastIndexOf(.)); } // 构建存储对象键格式为文件夹/用户ID-时间戳.扩展名 String objectKey props.getFolder() / userId - Instant.now().toEpochMilli() ext; // 创建OSS客户端 OSS client new OSSClientBuilder().build(props.getEndpoint(), props.getAccessKeyId(), props.getAccessKeySecret()); try { // 创建上传请求并执行上传 PutObjectRequest request new PutObjectRequest(props.getBucketName(), objectKey, file.getInputStream()); client.putObject(request); } catch (IOException e) { // 处理文件读取异常 throw new BusinessException(ErrorCode.BAD_REQUEST, 头像文件读取失败); } finally { // 确保关闭OSS客户端 client.shutdown(); } // 返回文件的公开访问URL return publicUrl(objectKey); } }