修复子任务积分计算错误的bug

问题:当子任务包含多个任务类型时,updateTaskItemsQuantityAndScore方法
只用第一个任务类型的单价计算总积分,导致积分显示错误。

解决:
1. 先更新typeDetails JSON,再从更新后的typeDetails中重新计算总积分
2. 新增calculateTotalScoreFromTypeDetails方法从JSON中累加所有类型积分
3. 新增calculateTotalQuantityFromTypeDetails方法计算总数量
4. 同时更新平均单价以保持数据一致性
This commit is contained in:
Your Name 2026-01-27 20:57:28 +08:00
parent 2d08524f41
commit 1d66f0d7f5
2 changed files with 197 additions and 30 deletions

104
CLAUDE.md Normal file
View File

@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
DesignHub is a full-stack Designer Task Management Platform (设计师任务抢单管理系统) - a task distribution and management system with a point-based incentive mechanism.
**Tech Stack:**
- **Frontend**: Vue 3 + TypeScript + Ant Design Vue + Pinia + Vite
- **Backend**: Spring Boot 3.x (Java 17) + MySQL + MyBatis Plus + Redis
- **Authentication**: JWT tokens with RBAC (7 roles)
## Common Commands
### Frontend (cd frontend)
```bash
pnpm install # Install dependencies
pnpm dev # Start dev server (port 3000)
pnpm build # Build for production
pnpm lint # Lint and fix code
pnpm format # Format with Prettier
```
### Backend (cd backend)
```bash
mvn clean install -DskipTests # Build project
mvn spring-boot:run # Start dev server (port 8080)
java -jar target/designhub-backend-*.jar # Run compiled JAR
```
### Database
```bash
mysql -u root -p < database/schema.sql
mysql -u root -p < database/init-data.sql
```
## Architecture
### Backend Structure
```
backend/src/main/java/com/designhub/
├── annotation/ # Custom annotations (@RequirePermission, @RequireRole)
├── common/ # Result wrapper, ResultCode enums
├── config/ # Spring configs (Cors, MyBatis, Swagger, Web)
├── controller/ # REST APIs (38 controllers)
├── dto/ # Request DTOs
├── entity/ # JPA entities (53 tables)
├── exception/ # Business exceptions
├── interceptor/ # Auth interceptor
├── mapper/ # MyBatis mappers
├── service/ # Business logic (41 services)
├── util/ # JwtUtil, ExcelExportUtil, etc.
└── vo/ # Response View Objects
```
### Frontend Structure
```
frontend/src/
├── api/ # Axios API modules (task.ts, user.ts, auth.ts, etc.)
├── components/ # Reusable components (TaskCard, Navbar, etc.)
├── composables/ # Vue composables
├── layouts/ # Layout wrappers
├── router/ # Vue Router config
├── stores/ # Pinia stores (auth, config, siteSettings)
├── types/ # TypeScript interfaces
├── utils/ # Utility functions
└── views/ # Page components (27 views)
```
### API Documentation
- Swagger UI: http://localhost:8080/doc.html (when backend running)
- Frontend dev server proxies `/api` to `http://localhost:8080`
## Key Conventions
### Backend
- Custom permission annotations: `@RequirePermission`, `@RequireRole` on controller methods
- Response format: `Result<T>` wrapper with `ResultCode` enum for status codes
- JWT tokens stored in `Authorization` header (Bearer scheme)
### Frontend
- Auto-imported Vue components via `unplugin-vue-components`
- API calls use axios with JWT in localStorage
- Vue Composition API (`<script setup>`)
### Git Commit Format
```
feat: 新功能
fix: Bug修复
docs: 文档更新
style: 代码格式
refactor: 重构
perf: 性能优化
test: 测试相关
```
## Entry Points
- Frontend main: `frontend/src/main.ts`
- Backend main: `backend/src/main/java/com/designhub/DesignHubApplication.java`
- Dev config: `backend/src/main/resources/application-dev.yml`
## Default Credentials
- Admin: `admin` / `123456`

View File

@ -1094,7 +1094,36 @@ public class TaskApprovalService {
for (TaskItem item : taskItems) {
boolean needUpdate = false;
// 检查子任务的 taskTypeId 是否在更新列表中
// 🔥 先更新 typeDetails JSON 字段(如果存在)
// 这样后面可以从更新后的 typeDetails 中正确计算总积分
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
String updatedTypeDetails = updateTypeDetailsJson(item.getTypeDetails(), typeUpdateMap);
if (!item.getTypeDetails().equals(updatedTypeDetails)) {
item.setTypeDetails(updatedTypeDetails);
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新子任务typeDetails: itemId={}, newTypeDetails={}", item.getId(), updatedTypeDetails);
// 🔥 从更新后的 typeDetails 中重新计算总积分和总数量
// 这是修复多任务类型子任务积分计算错误的关键
java.math.BigDecimal newTotalScore = calculateTotalScoreFromTypeDetails(updatedTypeDetails);
int newTotalQuantity = calculateTotalQuantityFromTypeDetails(updatedTypeDetails);
if (newTotalScore != null) {
item.setTotalScore(newTotalScore);
log.info("从typeDetails重新计算子任务总积分: itemId={}, newTotalScore={}", item.getId(), newTotalScore);
}
if (newTotalQuantity > 0) {
item.setQuantity(newTotalQuantity);
// 重新计算平均单价
if (newTotalScore != null && newTotalQuantity > 0) {
item.setUnitScore(newTotalScore.divide(new java.math.BigDecimal(newTotalQuantity), 2, java.math.RoundingMode.HALF_UP));
}
log.info("从typeDetails重新计算子任务总数量: itemId={}, newTotalQuantity={}", item.getId(), newTotalQuantity);
}
}
} else {
// 如果没有 typeDetails,使用原来的单类型逻辑
if (item.getTaskTypeId() != null && typeUpdateMap.containsKey(item.getTaskTypeId())) {
TaskUpdateDTO.TaskTypeUpdateDTO typeUpdate = typeUpdateMap.get(item.getTaskTypeId());
Integer newQuantity = typeUpdate.getQuantity();
@ -1103,7 +1132,6 @@ public class TaskApprovalService {
// 获取单价积分
java.math.BigDecimal unitScore = item.getUnitScore();
if (unitScore == null) {
// 如果没有单价,从配置中获取
DemandType demandType = configService.getDemandTypeById(item.getTaskTypeId());
if (demandType != null && demandType.getUnitScore() != null) {
unitScore = demandType.getUnitScore();
@ -1112,26 +1140,15 @@ public class TaskApprovalService {
}
}
// 更新数量和总积分
item.setQuantity(newQuantity);
item.setTotalScore(unitScore.multiply(new java.math.BigDecimal(newQuantity)));
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新子任务数量和积分: itemId={}, taskTypeId={}, oldQuantity={}, newQuantity={}, unitScore={}, newTotalScore={}",
item.getId(), item.getTaskTypeId(), item.getQuantity(), newQuantity, unitScore, item.getTotalScore());
log.info("更新单类型子任务数量和积分: itemId={}, taskTypeId={}, newQuantity={}, newTotalScore={}",
item.getId(), item.getTaskTypeId(), newQuantity, item.getTotalScore());
}
}
// 🔥 同时更新 typeDetails JSON 字段(如果存在)
if (item.getTypeDetails() != null && !item.getTypeDetails().isEmpty()) {
String updatedTypeDetails = updateTypeDetailsJson(item.getTypeDetails(), typeUpdateMap);
if (!item.getTypeDetails().equals(updatedTypeDetails)) {
item.setTypeDetails(updatedTypeDetails);
item.setUpdateTime(LocalDateTime.now());
needUpdate = true;
log.info("更新子任务typeDetails: itemId={}, newTypeDetails={}", item.getId(), updatedTypeDetails);
}
}
if (needUpdate) {
@ -1142,6 +1159,52 @@ public class TaskApprovalService {
log.info("完成子任务数量和积分更新: taskId={}, 子任务数量={}", taskId, taskItems.size());
}
/**
* 从 typeDetails JSON 中计算总积分
*/
private java.math.BigDecimal calculateTotalScoreFromTypeDetails(String typeDetails) {
try {
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(typeDetails);
if (!rootNode.isArray()) {
return null;
}
java.math.BigDecimal totalScore = java.math.BigDecimal.ZERO;
for (com.fasterxml.jackson.databind.JsonNode itemNode : rootNode) {
if (itemNode.has("totalScore")) {
totalScore = totalScore.add(new java.math.BigDecimal(itemNode.get("totalScore").asDouble()));
}
}
return totalScore.setScale(2, java.math.RoundingMode.HALF_UP);
} catch (Exception e) {
log.error("计算typeDetails总积分失败: {}", e.getMessage());
return null;
}
}
/**
* 从 typeDetails JSON 中计算总数量
*/
private int calculateTotalQuantityFromTypeDetails(String typeDetails) {
try {
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(typeDetails);
if (!rootNode.isArray()) {
return 0;
}
int totalQuantity = 0;
for (com.fasterxml.jackson.databind.JsonNode itemNode : rootNode) {
if (itemNode.has("quantity")) {
totalQuantity += itemNode.get("quantity").asInt();
}
}
return totalQuantity;
} catch (Exception e) {
log.error("计算typeDetails总数量失败: {}", e.getMessage());
return 0;
}
}
/**
* 更新 typeDetails JSON 中的数量
*/