恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
174 lines
7.4 KiB
Java
174 lines
7.4 KiB
Java
package com.kaidi.oa.web;
|
|
|
|
import com.kaidi.oa.common.ApiException;
|
|
import com.kaidi.oa.common.ApiResp;
|
|
import com.kaidi.oa.common.NotFoundException;
|
|
import com.kaidi.oa.domain.CultureMaterial;
|
|
import com.kaidi.oa.domain.CultureMaterialDownload;
|
|
import com.kaidi.oa.repository.CultureMaterialDownloadRepository;
|
|
import com.kaidi.oa.repository.CultureMaterialRepository;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
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.RequestParam;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
import java.time.Instant;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* 员工宣传素材授权下载追踪(品牌推广部·宣传部·模块7 缺口)。
|
|
*
|
|
* 补完需求:CultureMaterial.fileUrls 原仅存 URL 字符串,无版本授权下载追踪。
|
|
* 本控制器为每次"员工申请下载"建立授权记录,宣传部可审批(已授权/已拒绝),
|
|
* 审批通过后员工确认下载时记录 downloadAt 时间戳,形成完整的分发留痕。
|
|
*
|
|
* 已通过素材(CultureMaterial.status=已通过)可直接申请,自动置为「已授权」;
|
|
* 受控素材(如草稿/退回)需人工审批,防止未定稿素材外流。
|
|
*/
|
|
@RestController
|
|
@RequestMapping("/api/oa/culture-material-downloads")
|
|
public class CultureMaterialDownloadController {
|
|
|
|
private final CultureMaterialDownloadRepository dlRepo;
|
|
private final CultureMaterialRepository matRepo;
|
|
|
|
public CultureMaterialDownloadController(CultureMaterialDownloadRepository dlRepo,
|
|
CultureMaterialRepository matRepo) {
|
|
this.dlRepo = dlRepo;
|
|
this.matRepo = matRepo;
|
|
}
|
|
|
|
@GetMapping
|
|
public ApiResp<List<CultureMaterialDownload>> list(
|
|
@RequestParam(required = false) Long materialId,
|
|
@RequestParam(required = false) String downloader,
|
|
@RequestParam(required = false) String authStatus) {
|
|
if (materialId != null) {
|
|
return ApiResp.ok(dlRepo.findByMaterialId(materialId));
|
|
}
|
|
if (downloader != null && !downloader.isBlank()) {
|
|
return ApiResp.ok(dlRepo.findByDownloader(downloader));
|
|
}
|
|
if (authStatus != null && !authStatus.isBlank()) {
|
|
return ApiResp.ok(dlRepo.findByAuthStatus(authStatus));
|
|
}
|
|
return ApiResp.ok(dlRepo.findAll());
|
|
}
|
|
|
|
@GetMapping("/{id}")
|
|
public ApiResp<CultureMaterialDownload> get(@PathVariable Long id) {
|
|
return ApiResp.ok(require(id));
|
|
}
|
|
|
|
public record DownloadRequest(
|
|
Long materialId, String downloader, String downloaderDept,
|
|
String purpose, String version) {
|
|
}
|
|
|
|
/**
|
|
* 申请下载素材。
|
|
* - 素材为「已通过」状态时,自动置「已授权」,员工直接可下载;
|
|
* - 其他状态需宣传部手工授权(置「申请中」等待审批)。
|
|
*/
|
|
@PostMapping
|
|
@Transactional
|
|
public ApiResp<CultureMaterialDownload> apply(@RequestBody DownloadRequest req) {
|
|
if (req.materialId() == null) {
|
|
throw new ApiException(400, "materialId 不能为空");
|
|
}
|
|
if (req.downloader() == null || req.downloader().isBlank()) {
|
|
throw new ApiException(400, "申请人(downloader) 不能为空");
|
|
}
|
|
CultureMaterial mat = matRepo.findById(req.materialId())
|
|
.orElseThrow(() -> new NotFoundException("素材不存在:" + req.materialId()));
|
|
|
|
CultureMaterialDownload dl = new CultureMaterialDownload();
|
|
dl.setMaterialId(req.materialId());
|
|
dl.setMaterialTitle(mat.getTitle());
|
|
dl.setDownloader(req.downloader());
|
|
dl.setDownloaderDept(req.downloaderDept());
|
|
dl.setPurpose(req.purpose() == null ? "内部展示" : req.purpose());
|
|
dl.setVersion(req.version() == null ? "v1.0" : req.version());
|
|
dl.setCreatedAt(Instant.now());
|
|
|
|
// 已通过素材自动授权;否则人工审批
|
|
if ("已通过".equals(mat.getStatus())) {
|
|
dl.setAuthStatus("已授权");
|
|
dl.setAuthorizedAt(Instant.now());
|
|
dl.setAuthorizedBy("系统自动");
|
|
} else {
|
|
dl.setAuthStatus("申请中");
|
|
}
|
|
return ApiResp.ok(dlRepo.save(dl));
|
|
}
|
|
|
|
public record AuthRequest(String authorizedBy, String authComment, boolean approved) {
|
|
}
|
|
|
|
/** 宣传部审批:申请中 → 已授权 / 已拒绝。 */
|
|
@PostMapping("/{id}/authorize")
|
|
@Transactional
|
|
public ApiResp<CultureMaterialDownload> authorize(@PathVariable Long id,
|
|
@RequestBody AuthRequest req) {
|
|
CultureMaterialDownload dl = require(id);
|
|
if (!"申请中".equals(dl.getAuthStatus())) {
|
|
throw new ApiException(409, "仅「申请中」可审批,当前「" + dl.getAuthStatus() + "」");
|
|
}
|
|
if (req.authorizedBy() == null || req.authorizedBy().isBlank()) {
|
|
throw new ApiException(400, "授权人(authorizedBy) 不能为空");
|
|
}
|
|
dl.setAuthorizedBy(req.authorizedBy());
|
|
dl.setAuthComment(req.authComment());
|
|
dl.setAuthStatus(req.approved() ? "已授权" : "已拒绝");
|
|
if (req.approved()) {
|
|
dl.setAuthorizedAt(Instant.now());
|
|
}
|
|
return ApiResp.ok(dlRepo.save(dl));
|
|
}
|
|
|
|
/** 确认下载:已授权 → 记录 downloadAt 时间戳(员工实际点击下载时调用)。 */
|
|
@PostMapping("/{id}/confirm-download")
|
|
@Transactional
|
|
public ApiResp<CultureMaterialDownload> confirmDownload(@PathVariable Long id) {
|
|
CultureMaterialDownload dl = require(id);
|
|
if (!"已授权".equals(dl.getAuthStatus())) {
|
|
throw new ApiException(409, "仅已授权记录可确认下载,当前「" + dl.getAuthStatus() + "」");
|
|
}
|
|
if (dl.getDownloadAt() != null) {
|
|
throw new ApiException(409, "该授权记录已确认下载,请勿重复");
|
|
}
|
|
dl.setDownloadAt(Instant.now());
|
|
return ApiResp.ok(dlRepo.save(dl));
|
|
}
|
|
|
|
/** 素材下载统计(按素材汇总申请量/已授权量/实际下载量)。 */
|
|
public record DownloadStat(Long materialId, String materialTitle,
|
|
long applied, long authorized, long downloaded) {
|
|
}
|
|
|
|
@GetMapping("/stats")
|
|
public ApiResp<List<DownloadStat>> stats() {
|
|
List<CultureMaterialDownload> all = dlRepo.findAll();
|
|
java.util.Map<Long, DownloadStat> m = new java.util.LinkedHashMap<>();
|
|
for (CultureMaterialDownload dl : all) {
|
|
Long mid = dl.getMaterialId();
|
|
DownloadStat prev = m.getOrDefault(mid,
|
|
new DownloadStat(mid, dl.getMaterialTitle(), 0, 0, 0));
|
|
long app = prev.applied() + 1;
|
|
long auth = prev.authorized() + ("已授权".equals(dl.getAuthStatus()) ? 1 : 0);
|
|
long done = prev.downloaded() + (dl.getDownloadAt() != null ? 1 : 0);
|
|
m.put(mid, new DownloadStat(mid, dl.getMaterialTitle(), app, auth, done));
|
|
}
|
|
return ApiResp.ok(new java.util.ArrayList<>(m.values()));
|
|
}
|
|
|
|
private CultureMaterialDownload require(Long id) {
|
|
return dlRepo.findById(id)
|
|
.orElseThrow(() -> new NotFoundException("下载记录不存在:" + id));
|
|
}
|
|
}
|