SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)

恢复点(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>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,208 @@
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.ContentTask;
import com.kaidi.oa.repository.ContentTaskRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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;
/**
* 品牌推广部·内容创作任务分发(内容创作与审核发布·任务分发子功能)。
*
* 补全缺口:ContentPiece 仅有 author 字段,无独立 ContentTask 实体/控制器,
* 无任务指派给供应商的关联机制。
*
* 功能:
* 1. 内容创作任务 CRUD(文案/设计/视频,指派内部员工或外部供应商)
* 2. 状态机:待接受 → 进行中 → 待审核 → 已发布 / 已驳回
* 3. 任务列表支持按状态/类型/指派类型过滤
* 4. 完成后可关联 ContentPiece 稿件(contentCode/contentId)形成追溯链
*/
@RestController
@RequestMapping("/api/oa/content-tasks")
public class ContentTaskController {
private final ContentTaskRepository taskRepo;
public ContentTaskController(ContentTaskRepository taskRepo) {
this.taskRepo = taskRepo;
}
// ---------- 任务台账 ----------
@GetMapping
public ApiResp<List<ContentTask>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String taskType,
@RequestParam(required = false) String assigneeType) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(taskRepo.findByStatus(status));
}
if (taskType != null && !taskType.isBlank()) {
return ApiResp.ok(taskRepo.findByTaskType(taskType));
}
if (assigneeType != null && !assigneeType.isBlank()) {
return ApiResp.ok(taskRepo.findByAssigneeType(assigneeType));
}
return ApiResp.ok(taskRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<ContentTask> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record TaskRequest(
String code, String title, String contentCode, Long contentId,
String taskType, String description, String assigneeType,
String assignee, String supplierCode, String dueDate, String issuedBy) {
}
@PostMapping
public ApiResp<ContentTask> create(@RequestBody TaskRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "任务标题(title) 不能为空");
}
if (req.assignee() == null || req.assignee().isBlank()) {
throw new ApiException(400, "指派对象(assignee) 不能为空");
}
ContentTask t = new ContentTask();
t.setCode(req.code() == null || req.code().isBlank()
? "CT-" + (taskRepo.count() + 1) : req.code());
t.setTitle(req.title());
t.setContentCode(req.contentCode());
t.setContentId(req.contentId());
t.setTaskType(req.taskType() == null || req.taskType().isBlank() ? "文案" : req.taskType());
t.setDescription(req.description());
t.setAssigneeType(req.assigneeType() == null || req.assigneeType().isBlank() ? "内部员工" : req.assigneeType());
t.setAssignee(req.assignee());
t.setSupplierCode(req.supplierCode());
t.setDueDate(req.dueDate());
t.setIssuedBy(req.issuedBy());
t.setStatus("待接受");
t.setCreatedAt(Instant.now());
return ApiResp.ok(taskRepo.save(t));
}
@PatchMapping("/{id}")
public ApiResp<ContentTask> update(@PathVariable Long id, @RequestBody TaskRequest req) {
ContentTask t = find(id);
if ("已发布".equals(t.getStatus())) {
throw new ApiException(409, "已发布的任务不可编辑");
}
if (req.title() != null && !req.title().isBlank()) t.setTitle(req.title());
if (req.contentCode() != null) t.setContentCode(req.contentCode());
if (req.contentId() != null) t.setContentId(req.contentId());
if (req.taskType() != null && !req.taskType().isBlank()) t.setTaskType(req.taskType());
if (req.description() != null) t.setDescription(req.description());
if (req.assigneeType() != null && !req.assigneeType().isBlank()) t.setAssigneeType(req.assigneeType());
if (req.assignee() != null && !req.assignee().isBlank()) t.setAssignee(req.assignee());
if (req.supplierCode() != null) t.setSupplierCode(req.supplierCode());
if (req.dueDate() != null) t.setDueDate(req.dueDate());
return ApiResp.ok(taskRepo.save(t));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
ContentTask t = find(id);
if (!"待接受".equals(t.getStatus()) && !"已驳回".equals(t.getStatus())) {
throw new ApiException(409, "仅待接受/已驳回状态可删除");
}
taskRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 状态机 ----------
/** 接受任务:待接受 → 进行中。 */
@PostMapping("/{id}/accept")
public ApiResp<ContentTask> accept(@PathVariable Long id) {
ContentTask t = find(id);
if (!"待接受".equals(t.getStatus())) {
throw new ApiException(409, "仅待接受状态可接受,当前「" + t.getStatus() + "");
}
t.setStatus("进行中");
return ApiResp.ok(taskRepo.save(t));
}
public record SubmitDeliverableRequest(String deliverableUrl, String contentCode, Long contentId) {
}
/** 提交交付物:进行中 → 待审核,关联稿件。 */
@PostMapping("/{id}/submit")
@Transactional
public ApiResp<ContentTask> submit(@PathVariable Long id,
@RequestBody(required = false) SubmitDeliverableRequest req) {
ContentTask t = find(id);
if (!"进行中".equals(t.getStatus())) {
throw new ApiException(409, "仅进行中可提交交付物,当前「" + t.getStatus() + "");
}
if (req != null) {
if (req.deliverableUrl() != null) t.setDeliverableUrl(req.deliverableUrl());
if (req.contentCode() != null) t.setContentCode(req.contentCode());
if (req.contentId() != null) t.setContentId(req.contentId());
}
t.setStatus("待审核");
return ApiResp.ok(taskRepo.save(t));
}
public record ReviewRequest(String reviewNote) {
}
/** 审核通过:待审核 → 已发布。 */
@PostMapping("/{id}/approve")
public ApiResp<ContentTask> approve(@PathVariable Long id,
@RequestBody(required = false) ReviewRequest req) {
ContentTask t = find(id);
if (!"待审核".equals(t.getStatus())) {
throw new ApiException(409, "仅待审核可通过,当前「" + t.getStatus() + "");
}
if (req != null && req.reviewNote() != null) t.setReviewNote(req.reviewNote());
t.setStatus("已发布");
return ApiResp.ok(taskRepo.save(t));
}
/** 驳回:待审核 → 已驳回(驳回后可重新提交,状态回进行中需手动再接受)。 */
@PostMapping("/{id}/reject")
public ApiResp<ContentTask> reject(@PathVariable Long id,
@RequestBody(required = false) ReviewRequest req) {
ContentTask t = find(id);
if (!"待审核".equals(t.getStatus())) {
throw new ApiException(409, "仅待审核可驳回,当前「" + t.getStatus() + "");
}
if (req != null && req.reviewNote() != null) t.setReviewNote(req.reviewNote());
t.setStatus("已驳回");
return ApiResp.ok(taskRepo.save(t));
}
/** 驳回后重新接受:已驳回 → 进行中。 */
@PostMapping("/{id}/restart")
public ApiResp<ContentTask> restart(@PathVariable Long id) {
ContentTask t = find(id);
if (!"已驳回".equals(t.getStatus())) {
throw new ApiException(409, "仅已驳回任务可重启,当前「" + t.getStatus() + "");
}
t.setStatus("进行中");
return ApiResp.ok(taskRepo.save(t));
}
// ---------- 内部工具 ----------
private ContentTask find(Long id) {
return taskRepo.findById(id)
.orElseThrow(() -> new NotFoundException("content task not found: " + id));
}
}