micro-bundle/internal/dao/taskDao.go
2026-01-21 10:32:38 +08:00

1948 lines
81 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dao
import (
"fmt"
bundleConfig "micro-bundle/config"
"micro-bundle/internal/dto"
"micro-bundle/internal/model"
"micro-bundle/pkg/app"
commonErr "micro-bundle/pkg/err"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetPendingTaskLayout() (string, error) {
var rec model.TaskPendingLayout
if err := app.ModuleClients.TaskBenchDB.Model(&model.TaskPendingLayout{}).Where("id = ?", 1).First(&rec).Error; err != nil {
return "", commonErr.ReturnError(err, "查询待指派布局失败", "查询待指派布局失败: ")
}
return rec.Data, nil
}
func SetPendingTaskLayout(data string) error {
err := app.ModuleClients.TaskBenchDB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"data"}),
}).Create(&model.TaskPendingLayout{ID: 1, Data: data}).Error
if err != nil {
return commonErr.ReturnError(err, "保存待指派布局失败", "保存待指派布局失败: ")
}
return nil
}
func AddHiddenTaskAssignee(taskAssignee string, taskAssigneeNum string) error {
rec := &model.TaskAssigneeHidden{
TaskAssignee: taskAssignee,
TaskAssigneeNum: taskAssigneeNum,
HiddenAt: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := app.ModuleClients.TaskBenchDB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "task_assignee_num"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"task_assignee": taskAssignee,
"hidden_at": time.Now(),
"updated_at": time.Now(),
"deleted_at": 0,
}),
}).Create(rec).Error; err != nil {
return commonErr.ReturnError(err, "新增隐藏指派人失败", "新增隐藏指派人失败: ")
}
return nil
}
// GetArtistUploadStatsList 查询所有艺人的上传统计与额度信息(通过 BundleBalance 关联 CastWork/CastWorkAnalysis及 subNum 关联 TaskManagement
func GetArtistUploadStatsList(req *dto.TaskQueryRequest) ([]*dto.ArtistUploadStatsItem, int64, error) {
taskSchema := bundleConfig.Data.TaskBenchDB.DbName
nowMonth := time.Now().Format("2006-01")
cte := `WITH
-- 1. 获取每个用户最新的订单记录使用NOT EXISTS避免窗口函数重复物化
latest_bor AS (
SELECT bor.id, bor.uuid, bor.customer_id, bor.customer_num, bor.created_at
FROM bundle_order_records bor
WHERE bor.deleted_at IS NULL
AND bor.customer_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM bundle_order_records bor2
WHERE bor2.customer_id = bor.customer_id
AND bor2.deleted_at IS NULL
AND (bor2.created_at > bor.created_at OR (bor2.created_at = bor.created_at AND bor2.id > bor.id))
)
),
-- 2. 获取每个用户的最新月份
newest_month AS (
SELECT user_id, MAX(month) AS month
FROM bundle_balance
WHERE deleted_at IS NULL
GROUP BY user_id
),
-- 3. 活跃窗口包含所有必要字段作为核心CTE
active_windows AS (
SELECT
u.id AS user_id,
u.tel_num AS phone,
bor.uuid AS order_uuid,
bor.customer_num,
bb.start_at,
bb.expired_at,
bb.bundle_video_number, bb.bundle_limit_video_number, bb.bundle_limit_video_expired_number,
bb.increase_video_number, bb.increase_limit_video_number, bb.increase_limit_video_expired_number,
bb.manual_video_number, bb.manual_video_consumption_number,
bb.bundle_image_number, bb.bundle_limit_image_number, bb.bundle_limit_image_expired_number,
bb.increase_image_number, bb.increase_limit_image_number, bb.increase_limit_image_expired_number,
bb.manual_image_number, bb.manual_image_consumption_number,
bb.bundle_data_analysis_number, bb.bundle_limit_data_analysis_number, bb.bundle_limit_data_analysis_expired_number,
bb.increase_data_analysis_number, bb.increase_limit_data_analysis_number, bb.increase_limit_data_analysis_expired_number,
bb.manual_data_analysis_number, bb.manual_data_analysis_consumption_number,
rn.name AS user_name
FROM ` + "`micro-account`.`user`" + ` u
INNER JOIN ` + "`micro-account`.real_name" + ` rn ON rn.id = u.real_name_id AND rn.name IS NOT NULL AND rn.deleted_at = 0
INNER JOIN bundle_activate bc ON bc.user_id = u.id AND bc.activate = 2
INNER JOIN latest_bor bor ON bor.customer_id = u.id AND u.deleted_at = 0
INNER JOIN newest_month nm ON nm.user_id = u.id
INNER JOIN bundle_balance bb ON bb.user_id = u.id AND bb.order_uuid = bor.uuid AND bb.month = nm.month AND bb.deleted_at IS NULL
WHERE u.deleted_at = 0
AND DATE_ADD(UTC_TIMESTAMP(), INTERVAL 8 HOUR) BETWEEN bb.start_at AND bb.expired_at
),
-- 4. 每个订单的最新月份余额
latest_per_order AS (
SELECT bb_inner.user_id, bb_inner.order_uuid, MAX(bb_inner.month) AS max_month
FROM bundle_balance bb_inner
INNER JOIN bundle_order_records bor_inner ON bor_inner.uuid = bb_inner.order_uuid
AND bor_inner.deleted_at IS NULL
AND bor_inner.status = 2
WHERE bb_inner.deleted_at IS NULL
AND bb_inner.month <= ?
GROUP BY bb_inner.user_id, bb_inner.order_uuid
),
-- 5. 余额汇总
balance_sum AS (
SELECT
bb2.user_id,
SUM(bb2.bundle_video_number + bb2.monthly_bundle_limit_video_number + bb2.monthly_bundle_limit_expired_video_number
+ bb2.invalid_bundle_video_number + bb2.bundle_limit_video_consumption_number
+ bb2.bundle_limit_video_expired_consumption_number - bb2.monthly_bundle_limit_expired_video_consumption_number
- bb2.monthly_bundle_limit_video_consumption_number) AS video_sum,
SUM(bb2.increase_video_number + bb2.monthly_increase_limit_video_number + bb2.monthly_increase_limit_expired_video_number
+ bb2.invalid_increase_video_number + bb2.increase_limit_video_consumption_number
+ bb2.increase_limit_video_expired_consumption_number - bb2.monthly_increase_limit_expired_video_consumption_number
- bb2.monthly_increase_limit_video_consumption_number) AS increase_video_sum,
SUM(bb2.bundle_image_number + bb2.monthly_bundle_limit_image_number + bb2.monthly_bundle_limit_expired_image_number
+ bb2.invalid_bundle_image_number + bb2.bundle_limit_image_consumption_number
+ bb2.bundle_limit_image_expired_consumption_number - bb2.monthly_bundle_limit_expired_image_consumption_number
- bb2.monthly_bundle_limit_image_consumption_number) AS post_sum,
SUM(bb2.increase_image_number + bb2.monthly_increase_limit_image_number + bb2.monthly_increase_limit_expired_image_number
+ bb2.invalid_increase_image_number + bb2.increase_limit_image_consumption_number
+ bb2.increase_limit_image_expired_consumption_number - bb2.monthly_increase_limit_expired_image_consumption_number
- bb2.monthly_increase_limit_image_consumption_number) AS increase_post_sum,
SUM(bb2.bundle_data_analysis_number + bb2.monthly_bundle_limit_data_analysis_number
+ bb2.monthly_bundle_limit_expired_data_analysis_number + bb2.invalid_bundle_data_analysis_number
+ bb2.bundle_limit_data_analysis_consumption_number + bb2.bundle_limit_data_analysis_expired_consumption_number
- bb2.monthly_bundle_limit_expired_data_analysis_consumption_number
- bb2.monthly_bundle_limit_data_analysis_consumption_number) AS data_sum,
SUM(bb2.increase_data_analysis_number + bb2.monthly_increase_limit_data_analysis_number
+ bb2.monthly_increase_limit_expired_data_analysis_number + bb2.invalid_increase_data_analysis_number
+ bb2.increase_limit_data_analysis_consumption_number + bb2.increase_limit_data_analysis_expired_consumption_number
- bb2.monthly_increase_limit_expired_data_analysis_consumption_number
- bb2.monthly_increase_limit_data_analysis_consumption_number) AS increase_data_sum
FROM bundle_balance bb2
INNER JOIN latest_per_order lpo ON bb2.user_id = lpo.user_id
AND bb2.order_uuid = lpo.order_uuid
AND bb2.month = lpo.max_month
INNER JOIN bundle_order_records bor2 ON bor2.uuid = bb2.order_uuid
AND bor2.deleted_at IS NULL
AND bor2.status = 2
WHERE bb2.deleted_at IS NULL
AND DATE_ADD(UTC_TIMESTAMP(), INTERVAL 8 HOUR) BETWEEN bb2.start_at AND bb2.expired_at
GROUP BY bb2.user_id
),
-- 6. 作品统计(只统计 origin_uuid 为空的作品)
cw_agg AS (
SELECT
aw.user_id,
COUNT(CASE WHEN cw.work_category = 2 AND cw.deleted_at = 0 AND (cw.origin_uuid = '' OR cw.origin_uuid IS NULL) AND cw.submit_time BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS uploaded_video_count,
COUNT(CASE WHEN cw.work_category = 1 AND cw.deleted_at = 0 AND (cw.origin_uuid = '' OR cw.origin_uuid IS NULL) AND cw.submit_time BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS uploaded_post_count,
COUNT(CASE WHEN cw.work_category = 2 AND cw.cost = 1 AND cw.deleted_at = 0 AND (cw.origin_uuid = '' OR cw.origin_uuid IS NULL) AND cw.submit_time BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS released_video_consumed,
COUNT(CASE WHEN cw.work_category = 1 AND cw.cost = 1 AND cw.deleted_at = 0 AND (cw.origin_uuid = '' OR cw.origin_uuid IS NULL) AND cw.submit_time BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS released_post_consumed
FROM active_windows aw
LEFT JOIN cast_work cw ON cw.artist_phone COLLATE utf8mb4_general_ci = aw.phone COLLATE utf8mb4_general_ci
GROUP BY aw.user_id
),
-- 7. 数据分析作品统计(排除 work_analysis_status = 1 的数据分析)
cwa_agg AS (
SELECT
aw.user_id,
COUNT(CASE WHEN cwa.deleted_at = 0 AND (cwa.work_analysis_status IS NULL OR cwa.work_analysis_status != 1) AND cwa.submit_time BETWEEN UNIX_TIMESTAMP(aw.start_at) AND UNIX_TIMESTAMP(aw.expired_at) THEN 1 END) AS uploaded_data_count,
COUNT(CASE WHEN cwa.cost = 1 AND cwa.deleted_at = 0 AND (cwa.work_analysis_status IS NULL OR cwa.work_analysis_status != 1) AND cwa.submit_time BETWEEN UNIX_TIMESTAMP(aw.start_at) AND UNIX_TIMESTAMP(aw.expired_at) THEN 1 END) AS released_data_consumed
FROM active_windows aw
LEFT JOIN cast_work_analysis cwa ON cwa.artist_phone COLLATE utf8mb4_general_ci = aw.phone COLLATE utf8mb4_general_ci
GROUP BY aw.user_id
),
-- 8. 视频脚本统计
cvs_agg AS (
SELECT
aw.user_id,
COUNT(cvs.artist_uuid) AS uploaded_video_script_count
FROM active_windows aw
LEFT JOIN cast_video_script cvs ON CAST(aw.user_id AS CHAR) COLLATE utf8mb4_general_ci = cvs.artist_uuid COLLATE utf8mb4_general_ci
AND cvs.deleted_at = 0
AND cvs.created_at BETWEEN UNIX_TIMESTAMP(CONVERT_TZ(aw.start_at, '+00:00', '+00:00'))
AND UNIX_TIMESTAMP(CONVERT_TZ(aw.expired_at, '+00:00', '+00:00'))
GROUP BY aw.user_id
),
-- 9. 任务统计直接使用active_windows的customer_num避免重复JOIN latest_bor
tar_agg AS (
SELECT
aw.user_id,
COUNT(CASE WHEN tar.status = 1 AND tar.deleted_at = 0 AND tar.created_at BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS progress_task_count,
COUNT(CASE WHEN tar.status = 2 AND tar.deleted_at = 0 AND tar.created_at BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS complete_task_count
FROM active_windows aw
LEFT JOIN ` + "`" + taskSchema + "`" + `.task_assign_records tar ON tar.sub_num COLLATE utf8mb4_general_ci = aw.customer_num COLLATE utf8mb4_general_ci
GROUP BY aw.user_id
),
-- 10. 已指派且未完成的数量统计actual_status=1未完成或actual_status=3已中止排除actual_status=2实际已完成的
assigned_pending_agg AS (
SELECT
aw.user_id,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_video_script_count
ELSE 0
END
), 0) AS assigned_video_script_count,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_video_count
ELSE 0
END
), 0) AS assigned_video_count,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_post_count
ELSE 0
END
), 0) AS assigned_post_count,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_data_count
ELSE 0
END
), 0) AS assigned_data_count
FROM active_windows aw
LEFT JOIN ` + "`" + taskSchema + "`" + `.task_assign_records tar ON tar.sub_num COLLATE utf8mb4_general_ci = aw.customer_num COLLATE utf8mb4_general_ci
AND tar.actual_status IN (1, 3)
AND tar.deleted_at = 0
AND tar.created_at BETWEEN aw.start_at AND aw.expired_at
GROUP BY aw.user_id
),
-- 11. 任务管理信息
task_mgmt AS (
SELECT t.user_id, t.last_task_assignee, t.task_assignee_num
FROM ` + "`" + taskSchema + "`" + `.task_management t
INNER JOIN (
SELECT user_id, MAX(updated_at) AS max_updated_at
FROM ` + "`" + taskSchema + "`" + `.task_management
WHERE deleted_at = 0
GROUP BY user_id
) x ON x.user_id = t.user_id AND x.max_updated_at = t.updated_at
WHERE t.deleted_at = 0
)`
fromClause := `FROM active_windows aw
LEFT JOIN balance_sum bs ON bs.user_id = aw.user_id
LEFT JOIN task_mgmt tm ON tm.user_id = aw.user_id
LEFT JOIN cw_agg cw ON cw.user_id = aw.user_id
LEFT JOIN cwa_agg cwa ON cwa.user_id = aw.user_id
LEFT JOIN cvs_agg cvs ON cvs.user_id = aw.user_id
LEFT JOIN tar_agg ta ON ta.user_id = aw.user_id
LEFT JOIN assigned_pending_agg apa ON apa.user_id = aw.user_id`
whereParts := make([]string, 0, 4)
args := make([]interface{}, 0, 8)
args = append(args, nowMonth)
if req != nil && req.Keyword != "" {
like := "%" + req.Keyword + "%"
whereParts = append(whereParts, "(aw.customer_num LIKE ? OR aw.phone LIKE ? OR aw.user_name LIKE ?)")
args = append(args, like, like, like)
}
if req != nil && req.LastTaskAssignee != "" {
like := "%" + req.LastTaskAssignee + "%"
whereParts = append(whereParts, "tm.last_task_assignee LIKE ?")
args = append(args, like)
}
if req != nil && len(req.SubNums) > 0 {
whereParts = append(whereParts, "aw.customer_num IN ?")
args = append(args, req.SubNums)
}
whereClause := ""
if len(whereParts) > 0 {
whereClause = " WHERE " + strings.Join(whereParts, " AND ")
}
countSQL := cte + " SELECT COUNT(DISTINCT aw.customer_num) " + fromClause + whereClause
var total int64
if err := app.ModuleClients.BundleDB.Raw(countSQL, args...).Scan(&total).Error; err != nil {
return nil, 0, commonErr.ReturnError(err, "查询总数失败", "查询艺人上传统计总数失败: ")
}
orderClause := "aw.start_at DESC"
if req != nil && req.SortBy != "" {
sortType := req.SortType
if sortType != "asc" && sortType != "desc" && sortType != "ASC" && sortType != "DESC" {
sortType = "DESC"
}
allowed := map[string]bool{
"user_name": true,
"user_phone_number": true,
"customer_num": true,
"uploaded_video_count": true,
"uploaded_video_script_count": true,
"uploaded_post_count": true,
"uploaded_data_count": true,
"bundle_video_total": true,
"increase_video_total": true,
"released_video_total": true,
"pending_video_count": true,
"pending_video_script_count": true,
"bundle_post_total": true,
"increase_post_total": true,
"released_post_total": true,
"pending_post_count": true,
"bundle_data_total": true,
"increase_data_total": true,
"released_data_total": true,
"pending_data_count": true,
"last_task_assignee": true,
"task_assignee_num": true,
"progress_task_count": true,
"complete_task_count": true,
"allow_video_script_count": true,
"allow_video_count": true,
"allow_post_count": true,
"allow_data_count": true,
}
if allowed[req.SortBy] {
orderClause = fmt.Sprintf("%s %s", req.SortBy, sortType)
}
}
selectSQL := cte + ` SELECT
aw.user_name,
aw.phone AS user_phone_number,
aw.customer_num,
aw.start_at,
aw.expired_at,
COALESCE(cw.uploaded_video_count, 0) AS uploaded_video_count,
COALESCE(cvs.uploaded_video_script_count, 0) AS uploaded_video_script_count,
COALESCE(cw.uploaded_post_count, 0) AS uploaded_post_count,
COALESCE(cwa.uploaded_data_count, 0) AS uploaded_data_count,
(aw.bundle_video_number + aw.bundle_limit_video_number + aw.bundle_limit_video_expired_number) AS bundle_video_total,
(aw.increase_video_number + aw.increase_limit_video_number + aw.increase_limit_video_expired_number + aw.manual_video_number) AS increase_video_total,
GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) AS released_video_total,
GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) - COALESCE(cw.uploaded_video_count, 0) AS pending_video_count,
GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) - COALESCE(cvs.uploaded_video_script_count, 0) AS pending_video_script_count,
(aw.bundle_image_number + aw.bundle_limit_image_number + aw.bundle_limit_image_expired_number) AS bundle_post_total,
(aw.increase_image_number + aw.increase_limit_image_number + aw.increase_limit_image_expired_number + aw.manual_image_number) AS increase_post_total,
GREATEST(COALESCE(bs.post_sum, 0) + COALESCE(bs.increase_post_sum, 0) + aw.manual_image_number - aw.manual_image_consumption_number, 0) AS released_post_total,
GREATEST(COALESCE(bs.post_sum, 0) + COALESCE(bs.increase_post_sum, 0) + aw.manual_image_number - aw.manual_image_consumption_number, 0) - COALESCE(cw.uploaded_post_count, 0) AS pending_post_count,
(aw.bundle_data_analysis_number + aw.bundle_limit_data_analysis_number + aw.bundle_limit_data_analysis_expired_number) AS bundle_data_total,
(aw.increase_data_analysis_number + aw.increase_limit_data_analysis_number + aw.increase_limit_data_analysis_expired_number + aw.manual_data_analysis_number) AS increase_data_total,
GREATEST(COALESCE(bs.data_sum, 0) + COALESCE(bs.increase_data_sum, 0) + aw.manual_data_analysis_number - aw.manual_data_analysis_consumption_number, 0) AS released_data_total,
GREATEST(COALESCE(bs.data_sum, 0) + COALESCE(bs.increase_data_sum, 0) + aw.manual_data_analysis_number - aw.manual_data_analysis_consumption_number, 0) - COALESCE(cwa.uploaded_data_count, 0) AS pending_data_count,
tm.last_task_assignee,
tm.task_assignee_num,
COALESCE(ta.progress_task_count, 0) AS progress_task_count,
COALESCE(ta.complete_task_count, 0) AS complete_task_count,
(GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) - COALESCE(cvs.uploaded_video_script_count, 0)) - COALESCE(apa.assigned_video_script_count, 0) AS allow_video_script_count,
(GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) - COALESCE(cw.uploaded_video_count, 0)) - COALESCE(apa.assigned_video_count, 0) AS allow_video_count,
(GREATEST(COALESCE(bs.post_sum, 0) + COALESCE(bs.increase_post_sum, 0) + aw.manual_image_number - aw.manual_image_consumption_number, 0) - COALESCE(cw.uploaded_post_count, 0)) - COALESCE(apa.assigned_post_count, 0) AS allow_post_count,
(GREATEST(COALESCE(bs.data_sum, 0) + COALESCE(bs.increase_data_sum, 0) + aw.manual_data_analysis_number - aw.manual_data_analysis_consumption_number, 0) - COALESCE(cwa.uploaded_data_count, 0)) - COALESCE(apa.assigned_data_count, 0) AS allow_data_count
` + fromClause + whereClause + " ORDER BY " + orderClause
listArgs := make([]interface{}, 0, len(args)+2)
listArgs = append(listArgs, args...)
if req != nil && req.Page > 0 && req.PageSize > 0 {
offset := (req.Page - 1) * req.PageSize
selectSQL = selectSQL + " LIMIT ? OFFSET ?"
listArgs = append(listArgs, req.PageSize, offset)
}
items := make([]dto.ArtistUploadStatsItem, 0)
if err := app.ModuleClients.BundleDB.Raw(selectSQL, listArgs...).Scan(&items).Error; err != nil {
return nil, 0, commonErr.ReturnError(err, "查询艺人余额与上传数据失败", "查询艺人余额与上传数据失败: ")
}
resp := make([]*dto.ArtistUploadStatsItem, 0, len(items))
for i := range items {
resp = append(resp, &items[i])
}
return resp, total, nil
}
// GetPendingAssignBySubNums 查询指定艺人的可指派数量(可上传数 - 已指派且未完成的数量)
func GetPendingAssignBySubNums(subNums []string, page int, pageSize int) ([]*dto.ArtistPendingAssignItem, int64, error) {
if len(subNums) == 0 {
return []*dto.ArtistPendingAssignItem{}, 0, nil
}
taskSchema := bundleConfig.Data.TaskBenchDB.DbName
nowMonth := time.Now().Format("2006-01")
cte := `WITH
-- 1. 获取每个用户最新的订单记录使用NOT EXISTS避免窗口函数重复物化
latest_bor AS (
SELECT bor.id, bor.uuid, bor.customer_id, bor.customer_num, bor.created_at
FROM bundle_order_records bor
WHERE bor.deleted_at IS NULL
AND bor.customer_id IS NOT NULL
AND bor.customer_num IN ?
AND NOT EXISTS (
SELECT 1 FROM bundle_order_records bor2
WHERE bor2.customer_id = bor.customer_id
AND bor2.deleted_at IS NULL
AND (bor2.created_at > bor.created_at OR (bor2.created_at = bor.created_at AND bor2.id > bor.id))
)
),
-- 2. 获取每个用户的最新月份
newest_month AS (
SELECT user_id, MAX(month) AS month
FROM bundle_balance
WHERE deleted_at IS NULL
GROUP BY user_id
),
-- 3. 活跃窗口包含所有必要字段作为核心CTE
active_windows AS (
SELECT
u.id AS user_id,
u.tel_num AS phone,
bor.uuid AS order_uuid,
bor.customer_num,
bb.start_at,
bb.expired_at,
bb.manual_video_number, bb.manual_video_consumption_number,
bb.manual_image_number, bb.manual_image_consumption_number,
bb.manual_data_analysis_number, bb.manual_data_analysis_consumption_number,
rn.name AS user_name
FROM ` + "`micro-account`.`user`" + ` u
INNER JOIN ` + "`micro-account`.real_name" + ` rn ON rn.id = u.real_name_id AND rn.name IS NOT NULL
INNER JOIN bundle_activate bc ON bc.user_id = u.id AND bc.activate = 2
INNER JOIN latest_bor bor ON bor.customer_id = u.id
INNER JOIN newest_month nm ON nm.user_id = u.id
INNER JOIN bundle_balance bb ON bb.user_id = u.id AND bb.order_uuid = bor.uuid AND bb.month = nm.month AND bb.deleted_at IS NULL
WHERE u.deleted_at = 0
AND DATE_ADD(UTC_TIMESTAMP(), INTERVAL 8 HOUR) BETWEEN bb.start_at AND bb.expired_at
),
-- 4. 每个订单的最新月份余额
latest_per_order AS (
SELECT bb_inner.user_id, bb_inner.order_uuid, MAX(bb_inner.month) AS max_month
FROM bundle_balance bb_inner
INNER JOIN bundle_order_records bor_inner ON bor_inner.uuid = bb_inner.order_uuid
AND bor_inner.deleted_at IS NULL
AND bor_inner.status = 2
WHERE bb_inner.deleted_at IS NULL
AND bb_inner.month <= ?
GROUP BY bb_inner.user_id, bb_inner.order_uuid
),
-- 5. 余额汇总
balance_sum AS (
SELECT
bb2.user_id,
SUM(bb2.bundle_video_number + bb2.monthly_bundle_limit_video_number + bb2.monthly_bundle_limit_expired_video_number
+ bb2.invalid_bundle_video_number + bb2.bundle_limit_video_consumption_number
+ bb2.bundle_limit_video_expired_consumption_number - bb2.monthly_bundle_limit_expired_video_consumption_number
- bb2.monthly_bundle_limit_video_consumption_number) AS video_sum,
SUM(bb2.increase_video_number + bb2.monthly_increase_limit_video_number + bb2.monthly_increase_limit_expired_video_number
+ bb2.invalid_increase_video_number + bb2.increase_limit_video_consumption_number
+ bb2.increase_limit_video_expired_consumption_number - bb2.monthly_increase_limit_expired_video_consumption_number
- bb2.monthly_increase_limit_video_consumption_number) AS increase_video_sum,
SUM(bb2.bundle_image_number + bb2.monthly_bundle_limit_image_number + bb2.monthly_bundle_limit_expired_image_number
+ bb2.invalid_bundle_image_number + bb2.bundle_limit_image_consumption_number
+ bb2.bundle_limit_image_expired_consumption_number - bb2.monthly_bundle_limit_expired_image_consumption_number
- bb2.monthly_bundle_limit_image_consumption_number) AS post_sum,
SUM(bb2.increase_image_number + bb2.monthly_increase_limit_image_number + bb2.monthly_increase_limit_expired_image_number
+ bb2.invalid_increase_image_number + bb2.increase_limit_image_consumption_number
+ bb2.increase_limit_image_expired_consumption_number - bb2.monthly_increase_limit_expired_image_consumption_number
- bb2.monthly_increase_limit_image_consumption_number) AS increase_post_sum,
SUM(bb2.bundle_data_analysis_number + bb2.monthly_bundle_limit_data_analysis_number
+ bb2.monthly_bundle_limit_expired_data_analysis_number + bb2.invalid_bundle_data_analysis_number
+ bb2.bundle_limit_data_analysis_consumption_number + bb2.bundle_limit_data_analysis_expired_consumption_number
- bb2.monthly_bundle_limit_expired_data_analysis_consumption_number
- bb2.monthly_bundle_limit_data_analysis_consumption_number) AS data_sum,
SUM(bb2.increase_data_analysis_number + bb2.monthly_increase_limit_data_analysis_number
+ bb2.monthly_increase_limit_expired_data_analysis_number + bb2.invalid_increase_data_analysis_number
+ bb2.increase_limit_data_analysis_consumption_number + bb2.increase_limit_data_analysis_expired_consumption_number
- bb2.monthly_increase_limit_expired_data_analysis_consumption_number
- bb2.monthly_increase_limit_data_analysis_consumption_number) AS increase_data_sum
FROM bundle_balance bb2
INNER JOIN latest_per_order lpo ON bb2.user_id = lpo.user_id
AND bb2.order_uuid = lpo.order_uuid
AND bb2.month = lpo.max_month
INNER JOIN bundle_order_records bor2 ON bor2.uuid = bb2.order_uuid
AND bor2.deleted_at IS NULL
AND bor2.status = 2
WHERE bb2.deleted_at IS NULL
AND DATE_ADD(UTC_TIMESTAMP(), INTERVAL 8 HOUR) BETWEEN bb2.start_at AND bb2.expired_at
GROUP BY bb2.user_id
),
-- 6. 作品统计
cw_agg AS (
SELECT
aw.user_id,
COUNT(CASE WHEN cw.work_category = 2 AND cw.deleted_at = 0 AND cw.submit_time BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS uploaded_video_count,
COUNT(CASE WHEN cw.work_category = 1 AND cw.deleted_at = 0 AND cw.submit_time BETWEEN aw.start_at AND aw.expired_at THEN 1 END) AS uploaded_post_count
FROM active_windows aw
LEFT JOIN cast_work cw ON cw.artist_phone COLLATE utf8mb4_general_ci = aw.phone COLLATE utf8mb4_general_ci
GROUP BY aw.user_id
),
-- 7. 数据分析作品统计
cwa_agg AS (
SELECT
aw.user_id,
COUNT(CASE WHEN cwa.deleted_at = 0 AND cwa.submit_time BETWEEN UNIX_TIMESTAMP(aw.start_at) AND UNIX_TIMESTAMP(aw.expired_at) THEN 1 END) AS uploaded_data_count
FROM active_windows aw
LEFT JOIN cast_work_analysis cwa ON cwa.artist_phone COLLATE utf8mb4_general_ci = aw.phone COLLATE utf8mb4_general_ci
GROUP BY aw.user_id
),
-- 8. 视频脚本统计
cvs_agg AS (
SELECT
aw.user_id,
COUNT(cvs.artist_uuid) AS uploaded_video_script_count
FROM active_windows aw
LEFT JOIN cast_video_script cvs ON CAST(aw.user_id AS CHAR) COLLATE utf8mb4_general_ci = cvs.artist_uuid COLLATE utf8mb4_general_ci
AND cvs.deleted_at = 0
AND cvs.created_at BETWEEN UNIX_TIMESTAMP(CONVERT_TZ(aw.start_at, '+00:00', '+00:00'))
AND UNIX_TIMESTAMP(CONVERT_TZ(aw.expired_at, '+00:00', '+00:00'))
GROUP BY aw.user_id
),
-- 9. 已指派且未完成的数量统计actual_status=1未完成或actual_status=3已中止排除actual_status=2实际已完成的
assigned_pending_agg AS (
SELECT
aw.user_id,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_video_script_count
ELSE 0
END
), 0) AS assigned_video_script_count,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_video_count
ELSE 0
END
), 0) AS assigned_video_count,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_post_count
ELSE 0
END
), 0) AS assigned_post_count,
COALESCE(SUM(
CASE
WHEN tar.actual_status = 1 THEN tar.pending_data_count
ELSE 0
END
), 0) AS assigned_data_count
FROM active_windows aw
LEFT JOIN ` + "`" + taskSchema + "`" + `.task_assign_records tar ON tar.sub_num COLLATE utf8mb4_general_ci = aw.customer_num COLLATE utf8mb4_general_ci
AND tar.actual_status IN (1, 3)
AND tar.deleted_at = 0
AND tar.created_at BETWEEN aw.start_at AND aw.expired_at
GROUP BY aw.user_id
)`
fromClause := `FROM active_windows aw
LEFT JOIN balance_sum bs ON bs.user_id = aw.user_id
LEFT JOIN cw_agg cw ON cw.user_id = aw.user_id
LEFT JOIN cwa_agg cwa ON cwa.user_id = aw.user_id
LEFT JOIN cvs_agg cvs ON cvs.user_id = aw.user_id
LEFT JOIN assigned_pending_agg apa ON apa.user_id = aw.user_id`
args := make([]interface{}, 0, 4)
args = append(args, subNums)
args = append(args, nowMonth)
countSQL := cte + " SELECT COUNT(DISTINCT aw.customer_num) " + fromClause
var total int64
if err := app.ModuleClients.BundleDB.Raw(countSQL, args...).Scan(&total).Error; err != nil {
return nil, 0, commonErr.ReturnError(err, "查询可指派数量总数失败", "查询可指派数量总数失败: ")
}
selectSQL := cte + ` SELECT
aw.customer_num,
aw.phone AS tel_num,
aw.user_name,
(GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) - COALESCE(cvs.uploaded_video_script_count, 0)) - COALESCE(apa.assigned_video_script_count, 0) AS allow_video_script_count,
(GREATEST(COALESCE(bs.video_sum, 0) + COALESCE(bs.increase_video_sum, 0) + aw.manual_video_number - aw.manual_video_consumption_number, 0) - COALESCE(cw.uploaded_video_count, 0)) - COALESCE(apa.assigned_video_count, 0) AS allow_video_count,
(GREATEST(COALESCE(bs.post_sum, 0) + COALESCE(bs.increase_post_sum, 0) + aw.manual_image_number - aw.manual_image_consumption_number, 0) - COALESCE(cw.uploaded_post_count, 0)) - COALESCE(apa.assigned_post_count, 0) AS allow_post_count,
(GREATEST(COALESCE(bs.data_sum, 0) + COALESCE(bs.increase_data_sum, 0) + aw.manual_data_analysis_number - aw.manual_data_analysis_consumption_number, 0) - COALESCE(cwa.uploaded_data_count, 0)) - COALESCE(apa.assigned_data_count, 0) AS allow_data_count
` + fromClause + " ORDER BY aw.user_id"
listArgs := make([]interface{}, 0, len(args)+2)
listArgs = append(listArgs, args...)
if page > 0 && pageSize > 0 {
offset := (page - 1) * pageSize
selectSQL = selectSQL + " LIMIT ? OFFSET ?"
listArgs = append(listArgs, pageSize, offset)
}
items := make([]dto.ArtistPendingAssignItem, 0)
if err := app.ModuleClients.BundleDB.Raw(selectSQL, listArgs...).Scan(&items).Error; err != nil {
return nil, 0, commonErr.ReturnError(err, "查询可指派数量失败", "查询可指派数量失败: ")
}
resp := make([]*dto.ArtistPendingAssignItem, 0, len(items))
for i := range items {
resp = append(resp, &items[i])
}
return resp, total, nil
}
// AssignTask 指派某位员工完成某个艺人的任务
func AssignTask(req *dto.TaskAssignRequest, progressTaskCount int, completeTaskCount int) error {
// 开启事务
tx := app.ModuleClients.TaskBenchDB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. 查询当前艺人的任务记录
var taskManagement model.TaskManagement
err := tx.Where("sub_num = ? AND tel_num = ?", req.SubNum, req.TelNum).First(&taskManagement).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
var userID int
if req.SubNum != "" {
var row struct{ ID int }
_ = tx.Table("`micro-account`.`user`").Unscoped().Select("id").Where("sub_num COLLATE utf8mb4_general_ci = ? COLLATE utf8mb4_general_ci AND deleted_at = 0", req.SubNum).Limit(1).Scan(&row).Error
if row.ID > 0 {
userID = row.ID
}
}
if userID == 0 && req.SubNum != "" {
// 使用子查询获取每个 customer_id 最新订单记录的 customer_id
// 逻辑:找到 customer_num 匹配且 created_at 为该 customer_id 最大值的记录
var o struct{ CustomerID string }
subQuery := app.ModuleClients.BundleDB.Table("bundle_order_records").
Select("customer_id, MAX(created_at) AS max_created_time").
Where("deleted_at IS NULL AND status = 2").
Group("customer_id")
_ = app.ModuleClients.BundleDB.Table("bundle_order_records bor1").
Select("bor1.customer_id").
Joins("INNER JOIN (?) bor2 ON bor1.customer_id = bor2.customer_id AND bor1.created_at = bor2.max_created_time", subQuery).
Where("bor1.deleted_at IS NULL AND bor1.status = 2").
Where("bor1.customer_num COLLATE utf8mb4_general_ci = ? COLLATE utf8mb4_general_ci", req.SubNum).
Limit(1).
Scan(&o).Error
if o.CustomerID != "" {
if id64, _ := strconv.ParseInt(o.CustomerID, 10, 64); id64 > 0 {
userID = int(id64)
}
}
}
taskManagement = model.TaskManagement{
UserId: userID,
SubNum: req.SubNum,
TelNum: req.TelNum,
ArtistName: req.ArtistName,
ProgressCount: 0,
CompleteCount: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err = tx.Create(&taskManagement).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "创建任务记录失败", "创建任务记录失败: ")
}
} else {
tx.Rollback()
return commonErr.ReturnError(err, "查询任务记录失败", "查询任务记录失败: ")
}
}
// 2. 更新任务记录中的待办任务数量
updateData := map[string]interface{}{
"ProgressCount": progressTaskCount,
"CompleteCount": completeTaskCount,
"LastTaskAssignee": req.TaskAssignee,
"TaskAssigneeNum": req.TaskAssigneeNum,
"UpdatedAt": time.Now(),
}
if err = tx.Model(&taskManagement).Updates(updateData).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新任务记录失败", "更新任务记录失败: ")
}
if err = tx.Where("task_assignee_num = ? AND deleted_at = 0", req.TaskAssigneeNum).Delete(&model.TaskAssigneeHidden{}).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "移除隐藏指派人失败", "移除隐藏指派人失败: ")
}
// 5. 创建指派记录
if req.TaskBatch == "" {
req.TaskBatch = "未填写"
}
assignRecord := &model.TaskAssignRecords{
AssignRecordsUUID: uuid.New().String(), // 使用Google UUID
SubNum: req.SubNum,
TelNum: req.TelNum,
ArtistName: req.ArtistName,
TaskBatch: req.TaskBatch,
Status: 1, // 1:未完成
ActualStatus: 1, // 1:未完成
OperatorType: 2, // 2:指派
Operator: req.Operator, // 当前操作人名字
OperatorNum: req.OperatorNum, // 当前操作人账号
OperatorTime: time.Now(),
TaskAssignee: req.TaskAssignee, // 指派员工姓名
TaskAssigneeNum: req.TaskAssigneeNum, // 指派员工账号
PendingVideoScriptCount: req.AssignVideoScriptCount,
PendingVideoCount: req.AssignVideoCount,
PendingPostCount: req.AssignPostCount,
PendingDataCount: req.AssignDataCount,
AssignVideoScriptCount: req.AssignVideoScriptCount,
AssignVideoCount: req.AssignVideoCount,
AssignPostCount: req.AssignPostCount,
AssignDataCount: req.AssignDataCount,
CompleteVideoScriptCount: 0,
CompleteVideoCount: 0,
CompletePostCount: 0,
CompleteDataCount: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err = tx.Create(assignRecord).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "创建指派记录失败", "创建指派记录失败: ")
}
// 提交事务
if err = tx.Commit().Error; err != nil {
return commonErr.ReturnError(err, "提交事务失败", "提交事务失败: ")
}
return nil
}
func RevertTaskCompletionByUUIDItem(uuid string) error {
tx := app.ModuleClients.TaskBenchDB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var item model.TaskAssignUUIDItems
if err := tx.Where("uuid = ?", uuid).First(&item).Error; err != nil {
if err == gorm.ErrRecordNotFound {
tx.Rollback()
return nil
}
tx.Rollback()
return commonErr.ReturnError(err, "该UUID没有对应的任务uuid记录", "查询任务UUID记录失败: ")
}
if err := tx.Where("uuid = ?", uuid).Delete(&model.TaskAssignUUIDItems{}).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "软删除任务UUID记录失败", "软删除任务UUID记录失败: ")
}
var assignRecord model.TaskAssignRecords
if err := tx.Where("assign_records_uuid = ?", item.AssignRecordsUUID).First(&assignRecord).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "查询指派记录失败", "查询指派记录失败: ")
}
// now := time.Now()
updateData := map[string]interface{}{
"updated_at": assignRecord.UpdatedAt,
}
// 如果记录是 3的话就不改变直接返回
if assignRecord.ActualStatus == 3 {
return nil
}
if assignRecord.ActualStatus == 2 {
updateData["actual_status"] = 1
}
switch item.Type {
case 1:
updateData["pending_video_count"] = assignRecord.PendingVideoCount + 1
cv := assignRecord.CompleteVideoCount - 1
if cv < 0 {
cv = 0
}
updateData["complete_video_count"] = cv
case 2:
updateData["pending_post_count"] = assignRecord.PendingPostCount + 1
cp := assignRecord.CompletePostCount - 1
if cp < 0 {
cp = 0
}
updateData["complete_post_count"] = cp
case 3:
updateData["pending_data_count"] = assignRecord.PendingDataCount + 1
cd := assignRecord.CompleteDataCount - 1
if cd < 0 {
cd = 0
}
updateData["complete_data_count"] = cd
case 4:
updateData["pending_video_script_count"] = assignRecord.PendingVideoScriptCount + 1
cs := assignRecord.CompleteVideoScriptCount - 1
if cs < 0 {
cs = 0
}
updateData["complete_video_script_count"] = cs
}
if err := tx.Model(&assignRecord).Updates(updateData).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新指派记录失败", "更新指派记录失败: ")
}
if err := tx.Commit().Error; err != nil {
return commonErr.ReturnError(err, "提交事务失败", "提交事务失败: ")
}
return nil
}
// BatchAssignTasks 批量指派
func BatchAssignTasks(items []*dto.BatchAssignItem) error {
if len(items) == 0 {
return commonErr.ReturnError(nil, "参数错误", "批量指派项不能为空")
}
now := time.Now()
subNumSet := make(map[string]struct{})
subTelKeys := make(map[string]*dto.BatchAssignItem) // key: sub_num|tel_num
for _, it := range items {
if it == nil {
return commonErr.ReturnError(nil, "参数错误", "存在空的指派项")
}
// 校验数量不可为负且不可全部为0
if it.AssignVideoCount < 0 || it.AssignPostCount < 0 || it.AssignDataCount < 0 || it.AssignVideoScriptCount < 0 ||
(it.AssignVideoCount == 0 && it.AssignPostCount == 0 && it.AssignDataCount == 0 && it.AssignVideoScriptCount == 0) {
errMsg := fmt.Sprintf("\"%s\"老师任务不可全部为0", it.ArtistName)
return commonErr.ReturnError(nil, errMsg, "批量指派校验失败: ")
}
if it.TaskBatch == "" {
it.TaskBatch = "未填写"
}
if it.SubNum != "" {
subNumSet[it.SubNum] = struct{}{}
}
key := it.SubNum + "|" + it.TelNum
subTelKeys[key] = it
}
// 提取 subNums 列表
subNums := make([]string, 0, len(subNumSet))
for sn := range subNumSet {
subNums = append(subNums, sn)
}
// 2.1 批量查询已存在的 task_management 记录
existingTMMap := make(map[string]*model.TaskManagement) // key: sub_num|tel_num
if len(subTelKeys) > 0 {
var existingTMs []model.TaskManagement
// 构建 OR 条件查询
var conditions []string
var args []interface{}
for key := range subTelKeys {
parts := strings.SplitN(key, "|", 2)
conditions = append(conditions, "(sub_num = ? AND tel_num = ?)")
args = append(args, parts[0], parts[1])
}
query := strings.Join(conditions, " OR ")
if err := app.ModuleClients.TaskBenchDB.Where(query, args...).Find(&existingTMs).Error; err != nil {
return commonErr.ReturnError(err, "批量查询任务记录失败", "批量查询任务记录失败: ")
}
for i := range existingTMs {
tm := &existingTMs[i]
key := tm.SubNum + "|" + tm.TelNum
existingTMMap[key] = tm
}
}
// 2.2 批量查询 user_id从 micro-account.user 表)
userIDMap := make(map[string]int) // key: sub_num
if len(subNums) > 0 {
var userRows []struct {
SubNum string `gorm:"column:sub_num"`
ID int `gorm:"column:id"`
}
_ = app.ModuleClients.TaskBenchDB.Table("`micro-account`.`user`").Unscoped().
Select("sub_num, id").
Where("sub_num COLLATE utf8mb4_general_ci IN ? AND deleted_at = 0", subNums).
Scan(&userRows).Error
for _, row := range userRows {
if row.ID > 0 {
userIDMap[row.SubNum] = row.ID
}
}
}
// 2.3 批量查询 customer_id从 bundle_order_records 表,针对未找到 user_id 的 sub_num
missingSubNums := make([]string, 0)
for _, sn := range subNums {
if _, found := userIDMap[sn]; !found {
missingSubNums = append(missingSubNums, sn)
}
}
if len(missingSubNums) > 0 {
var orderRows []struct {
CustomerNum string `gorm:"column:customer_num"`
CustomerID string `gorm:"column:customer_id"`
}
// 使用子查询获取每个 customer_num 对应的最新订单的 customer_id
subQuery := app.ModuleClients.BundleDB.Table("bundle_order_records").
Select("customer_id, customer_num, MAX(created_at) AS max_created_time").
Where("deleted_at IS NULL AND status = 2").
Where("customer_num COLLATE utf8mb4_general_ci IN ?", missingSubNums).
Group("customer_id, customer_num")
_ = app.ModuleClients.BundleDB.Table("bundle_order_records bor1").
Select("bor1.customer_num, bor1.customer_id").
Joins("INNER JOIN (?) bor2 ON bor1.customer_id = bor2.customer_id AND bor1.created_at = bor2.max_created_time", subQuery).
Where("bor1.deleted_at IS NULL AND bor1.status = 2").
Where("bor1.customer_num COLLATE utf8mb4_general_ci IN ?", missingSubNums).
Scan(&orderRows).Error
for _, row := range orderRows {
if row.CustomerID != "" {
if id64, _ := strconv.ParseInt(row.CustomerID, 10, 64); id64 > 0 {
// 只有当 userIDMap 中没有时才设置
if _, found := userIDMap[row.CustomerNum]; !found {
userIDMap[row.CustomerNum] = int(id64)
}
}
}
}
}
// 2.4 批量统计每个 sub_num 的进行中与已完成任务数量
statsMap := make(map[string]struct {
ProgressTaskCount int
CompleteTaskCount int
})
if len(subNums) > 0 {
var statRows []struct {
SubNum string `gorm:"column:sub_num"`
ProgressTaskCount int `gorm:"column:progress_task_count"`
CompleteTaskCount int `gorm:"column:complete_task_count"`
}
if err := app.ModuleClients.TaskBenchDB.Table("task_assign_records").
Select("sub_num, COUNT(CASE WHEN status = 1 THEN 1 END) as progress_task_count, COUNT(CASE WHEN status = 2 THEN 1 END) as complete_task_count").
Where("sub_num IN ? AND deleted_at = 0", subNums).
Group("sub_num").
Scan(&statRows).Error; err != nil {
return commonErr.ReturnError(err, "批量查询艺人任务指派记录失败", "批量查询艺人任务指派记录失败: ")
}
for _, row := range statRows {
statsMap[row.SubNum] = struct {
ProgressTaskCount int
CompleteTaskCount int
}{
ProgressTaskCount: row.ProgressTaskCount,
CompleteTaskCount: row.CompleteTaskCount,
}
}
}
tx := app.ModuleClients.TaskBenchDB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 3.1 准备需要创建的 task_management 记录
var newTMs []model.TaskManagement
newTMMap := make(map[string]*model.TaskManagement) // key: sub_num|tel_num用于后续更新时引用
for _, it := range items {
key := it.SubNum + "|" + it.TelNum
if _, exists := existingTMMap[key]; !exists {
// 检查是否已经在 newTMs 中(同一批次可能有重复的 sub_num|tel_num
if _, inNew := newTMMap[key]; !inNew {
userID := userIDMap[it.SubNum]
tm := model.TaskManagement{
UserId: userID,
SubNum: it.SubNum,
TelNum: it.TelNum,
ArtistName: it.ArtistName,
ProgressCount: 0,
CompleteCount: 0,
CreatedAt: now,
UpdatedAt: now,
}
newTMs = append(newTMs, tm)
newTMMap[key] = &newTMs[len(newTMs)-1]
}
}
}
// 3.2 批量创建新的 task_management 记录
if len(newTMs) > 0 {
if err := tx.CreateInBatches(&newTMs, 100).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "批量创建任务记录失败", "批量创建任务记录失败: ")
}
// 更新 newTMMap 中的 IDCreateInBatches 会填充 ID
for i := range newTMs {
key := newTMs[i].SubNum + "|" + newTMs[i].TelNum
newTMMap[key] = &newTMs[i]
}
}
// 3.3 准备指派记录和更新数据
// 统计每个 sub_num 在本批次中新增的指派数量(用于计算 progress_count
subNumAssignCount := make(map[string]int)
for _, it := range items {
subNumAssignCount[it.SubNum]++
}
// 准备批量插入的指派记录
assignRecords := make([]model.TaskAssignRecords, 0, len(items))
for _, it := range items {
rec := model.TaskAssignRecords{
AssignRecordsUUID: uuid.New().String(),
SubNum: it.SubNum,
TelNum: it.TelNum,
ArtistName: it.ArtistName,
TaskBatch: it.TaskBatch,
Status: 1, // 未完成
ActualStatus: 1, // 未完成
OperatorType: 2, // 指派
Operator: it.Operator,
OperatorNum: it.OperatorNum,
OperatorTime: now,
TaskAssignee: it.TaskAssignee,
TaskAssigneeNum: it.TaskAssigneeNum,
PendingVideoScriptCount: it.AssignVideoScriptCount,
PendingVideoCount: it.AssignVideoCount,
PendingPostCount: it.AssignPostCount,
PendingDataCount: it.AssignDataCount,
AssignVideoScriptCount: it.AssignVideoScriptCount,
AssignVideoCount: it.AssignVideoCount,
AssignPostCount: it.AssignPostCount,
AssignDataCount: it.AssignDataCount,
CompleteVideoScriptCount: 0,
CompleteVideoCount: 0,
CompletePostCount: 0,
CompleteDataCount: 0,
CreatedAt: now,
UpdatedAt: now,
}
assignRecords = append(assignRecords, rec)
}
// 3.4 批量插入指派记录
assigneeNumsSet := make(map[string]struct{})
for _, it := range items {
if it.TaskAssigneeNum != "" {
assigneeNumsSet[it.TaskAssigneeNum] = struct{}{}
}
}
assigneeNums := make([]string, 0, len(assigneeNumsSet))
for num := range assigneeNumsSet {
assigneeNums = append(assigneeNums, num)
}
if len(assigneeNums) > 0 {
if err := tx.Where("task_assignee_num IN ? AND deleted_at = 0", assigneeNums).Delete(&model.TaskAssigneeHidden{}).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "移除隐藏指派人失败", "移除隐藏指派人失败: ")
}
}
if len(assignRecords) > 0 {
if err := tx.CreateInBatches(&assignRecords, 100).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "批量创建指派记录失败", "批量创建指派记录失败: ")
}
}
// 3.5 批量更新 task_management 表
updateGroups := make(map[string]*dto.BatchAssignItem)
for _, it := range items {
key := it.SubNum + "|" + it.TelNum
updateGroups[key] = it
}
for key, it := range updateGroups {
stats := statsMap[it.SubNum]
newProgressCount := stats.ProgressTaskCount + subNumAssignCount[it.SubNum]
// 获取对应的 task_management 记录 ID
var tmID int64
if tm, exists := existingTMMap[key]; exists {
tmID = tm.ID
} else if tm, exists := newTMMap[key]; exists {
tmID = tm.ID
}
if tmID > 0 {
updateData := map[string]interface{}{
"progress_count": newProgressCount,
"complete_count": stats.CompleteTaskCount,
"last_task_assignee": it.TaskAssignee,
"task_assignee_num": it.TaskAssigneeNum,
"updated_at": now,
}
if err := tx.Model(&model.TaskManagement{}).Where("id = ?", tmID).Updates(updateData).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新任务记录失败", "更新任务记录失败: ")
}
}
}
if err := tx.Commit().Error; err != nil {
return commonErr.ReturnError(err, "提交事务失败", "提交批量指派事务失败: ")
}
return nil
}
// GetRecentAssignRecords 查询最近被指派记录
func GetRecentAssignRecords(limit int) ([]*model.TaskAssignRecords, error) {
var records []*model.TaskAssignRecords
err := app.ModuleClients.TaskBenchDB.Raw(`
SELECT t1.*
FROM task_assign_records t1
WHERE t1.operator_type = ? AND t1.deleted_at = 0
AND NOT EXISTS (
SELECT 1 FROM task_assign_records t2
WHERE t2.task_assignee_num = t1.task_assignee_num
AND t2.operator_type = ? AND t2.deleted_at = 0
AND (t2.operator_time > t1.operator_time
OR (t2.operator_time = t1.operator_time AND t2.id > t1.id))
)
AND NOT EXISTS (
SELECT 1 FROM task_assignee_hidden h
WHERE h.task_assignee_num = t1.task_assignee_num
AND h.deleted_at = 0
)
ORDER BY t1.operator_time DESC, t1.id DESC
LIMIT ?
`, 2, 2, limit).Scan(&records).Error
if err != nil {
return nil, commonErr.ReturnError(err, "查询最近指派记录失败", "查询最近指派记录失败: ")
}
return records, nil
}
// GetEmployeeAssignedTasks 根据登录人信息查询被指派给该员工的艺人任务
func GetEmployeeAssignedTasks(req *dto.EmployeeTaskQueryRequest) ([]*model.TaskAssignRecords, int64, error) {
var records []*model.TaskAssignRecords
var total int64
// 构建查询条件
query := app.ModuleClients.TaskBenchDB.Model(&model.TaskAssignRecords{}).
Select("task_assign_records.id, task_assign_records.assign_records_uuid, task_assign_records.sub_num, task_assign_records.tel_num, task_assign_records.task_batch, task_assign_records.status, task_assign_records.actual_status, task_assign_records.complete_time, task_assign_records.operator_type, task_assign_records.operator, task_assign_records.operator_id, task_assign_records.operator_num, task_assign_records.operator_time, task_assign_records.task_assignee, task_assign_records.task_assignee_num, task_assign_records.pending_video_script_count, task_assign_records.pending_video_count, task_assign_records.pending_post_count, task_assign_records.pending_data_count, task_assign_records.assign_video_script_count, task_assign_records.assign_video_count, task_assign_records.assign_post_count, task_assign_records.assign_data_count, task_assign_records.complete_video_script_count, task_assign_records.complete_video_count, task_assign_records.complete_post_count, task_assign_records.complete_data_count, task_assign_records.created_at, task_assign_records.updated_at, task_assign_records.deleted_at, rn.name AS artist_name").
Joins("LEFT JOIN `micro-account`.`user` u ON u.sub_num COLLATE utf8mb4_general_ci = task_assign_records.sub_num COLLATE utf8mb4_general_ci").
Joins("LEFT JOIN `micro-account`.real_name rn ON u.real_name_id = rn.id").
Where("u.deleted_at = 0").
Where("task_assign_records.task_assignee_num = ?", req.TaskAssigneeNum)
// 关键词搜索(艺人姓名、编号、手机号)
if req.Keyword != "" {
query = query.Where("task_assign_records.sub_num LIKE ? OR task_assign_records.tel_num LIKE ? OR rn.name LIKE ?",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 指派人操作人姓名
if req.Operator != "" {
query = query.Where("task_assign_records.operator LIKE ?", "%"+req.Operator+"%")
}
// 任务批次
if req.TaskBatch != "" {
query = query.Where("task_assign_records.task_batch LIKE ?", "%"+req.TaskBatch+"%")
}
// 指派时间区间
if req.StartTime != "" && req.EndTime != "" {
query = query.Where("task_assign_records.operator_time BETWEEN ? AND ?", req.StartTime, req.EndTime)
} else if req.StartTime != "" {
query = query.Where("task_assign_records.operator_time >= ?", req.StartTime)
} else if req.EndTime != "" {
query = query.Where("task_assign_records.operator_time <= ?", req.EndTime)
}
// 完成时间区间
if req.StartCompleteTime != "" && req.EndCompleteTime != "" {
query = query.Where("task_assign_records.complete_time BETWEEN ? AND ?", req.StartCompleteTime, req.EndCompleteTime)
} else if req.StartCompleteTime != "" {
query = query.Where("task_assign_records.complete_time >= ?", req.StartCompleteTime)
} else if req.EndCompleteTime != "" {
query = query.Where("task_assign_records.complete_time <= ?", req.EndCompleteTime)
}
// 反馈完成状态
if req.Status != 0 {
query = query.Where("task_assign_records.status = ?", req.Status)
}
// 根据排序字段倒序
if req.SortBy != "" {
if strings.EqualFold(req.SortBy, "artist_name") {
query = query.Order("rn.name DESC")
} else {
query = query.Order("task_assign_records." + req.SortBy + " DESC")
}
}
// 计算总数
query.Count(&total)
// 分页
if req.PageSize > 0 && req.Page > 0 {
offset := (req.Page - 1) * req.PageSize
query = query.Limit(req.PageSize).Offset(offset)
}
// 按操作时间倒序
err := query.Order("task_assign_records.operator_time DESC").Find(&records).Error
if err != nil {
return nil, 0, commonErr.ReturnError(err, "查询员工指派任务失败", "查询员工指派任务失败: ")
}
return records, total, nil
}
// CompleteTaskManually 员工手动点击完成任务(同时同步任务管理表的进度与完成数量)
func CompleteTaskManually(assignRecordsUUID string) error {
// 开启事务,确保指派记录更新与任务管理同步原子性
tx := app.ModuleClients.TaskBenchDB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1) 查询该指派记录,获取艺人信息
var assignRecord model.TaskAssignRecords
if err := tx.Where("assign_records_uuid = ?", assignRecordsUUID).First(&assignRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
tx.Rollback()
return commonErr.ReturnError(nil, "未找到任务记录", "未找到指派记录: ")
}
tx.Rollback()
return commonErr.ReturnError(err, "查询指派记录失败", "查询指派记录失败: ")
}
// 2) 更新该记录为完成状态
now := time.Now()
updateData := map[string]interface{}{
"status": 2, // 2:完成
"complete_time": &now,
"updated_at": now,
}
if err := tx.Model(&assignRecord).Updates(updateData).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新任务完成状态失败", "更新任务完成状态失败: ")
}
// 3) 统计该艺人按照艺人sub_num 当前进行中与已完成数量
var stats struct {
ProgressTaskCount int
CompleteTaskCount int
}
if err := tx.Table("task_assign_records").
Select("COUNT(CASE WHEN status = 1 THEN 1 END) as progress_task_count, COUNT(CASE WHEN status = 2 THEN 1 END) as complete_task_count").
Where("sub_num = ? AND deleted_at = 0", assignRecord.SubNum).
Scan(&stats).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "查询艺人任务指派记录失败", "查询艺人任务指派记录失败: ")
}
// 4) 同步任务管理表的进行中与已完成数量(若不存在则创建)
var taskManagement model.TaskManagement
if err := tx.Where("sub_num = ? AND tel_num = ?", assignRecord.SubNum, assignRecord.TelNum).First(&taskManagement).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// 创建新记录
taskManagement = model.TaskManagement{
SubNum: assignRecord.SubNum,
TelNum: assignRecord.TelNum,
ArtistName: assignRecord.ArtistName,
ProgressCount: stats.ProgressTaskCount,
CompleteCount: stats.CompleteTaskCount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&taskManagement).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "创建任务记录失败", "创建任务记录失败: ")
}
} else {
tx.Rollback()
return commonErr.ReturnError(err, "查询任务记录失败", "查询任务记录失败: ")
}
} else {
updateTM := map[string]interface{}{
"ProgressCount": stats.ProgressTaskCount,
"CompleteCount": stats.CompleteTaskCount,
"UpdatedAt": time.Now(),
}
if err := tx.Model(&taskManagement).Updates(updateTM).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新任务记录失败", "更新任务记录失败: ")
}
}
// 5) 提交事务
if err := tx.Commit().Error; err != nil {
return commonErr.ReturnError(err, "提交事务失败", "提交事务失败: ")
}
return nil
}
// UpdateTaskActualStatusByUUID 根据指派记录UUID更新实际完成状态支持设置为未完成/完成/已中止)
func UpdateTaskActualStatusByUUID(assignRecordsUUID string, actualStatus int) error {
// 开启事务,确保更新原子性
tx := app.ModuleClients.TaskBenchDB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1) 查询该指派记录
var assignRecord model.TaskAssignRecords
if err := tx.Where("assign_records_uuid = ?", assignRecordsUUID).First(&assignRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
tx.Rollback()
return commonErr.ReturnError(nil, "未找到任务记录", "未找到指派记录: ")
}
tx.Rollback()
return commonErr.ReturnError(err, "查询指派记录失败", "查询指派记录失败: ")
}
// 如果目标状态为中止(3),且该任务已实际完成(2),则不允许中止
if actualStatus == 3 && assignRecord.ActualStatus == 2 {
tx.Rollback()
return commonErr.ReturnError(nil, "该任务已实际完成", "该任务已实际完成,不能中止: ")
}
// 2) 更新实际完成状态
now := time.Now()
updateData := map[string]interface{}{
"actual_status": actualStatus,
"updated_at": now,
}
if err := tx.Model(&assignRecord).Updates(updateData).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新实际完成状态失败", "更新实际完成状态失败: ")
}
// 3) 提交事务
if err := tx.Commit().Error; err != nil {
return commonErr.ReturnError(err, "提交事务失败", "提交事务失败: ")
}
return nil
}
// UpdateTaskProgress 员工实际完成任务状态更新
// 员工调用视频、图文、数据时,对应的待完成数据减一,已完成数据加一
func UpdateTaskProgress(req *dto.CompleteTaskRequest) error {
// 开启事务
tx := app.ModuleClients.TaskBenchDB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. 查询指派记录
var assignRecord model.TaskAssignRecords
var err error
if req.AssignRecordsUUID != "" {
// 如果提供了UUID直接根据UUID查询
err = tx.Where("assign_records_uuid = ?", req.AssignRecordsUUID).First(&assignRecord).Error
if err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "查询指派记录失败", "查询指派记录失败: ")
}
} else {
// 如果没有提供UUID根据员工信息与任务类型筛选最早的未手动完成任务
if req.EmployeeName == "" || req.EmployeeNum == "" {
tx.Rollback()
return commonErr.ReturnError(nil, "参数错误", "员工姓名和手机号不能为空")
}
// 仅选择“未手动完成”的记录
query := tx.Where("task_assignee = ? AND task_assignee_num = ? AND status = 1",
req.EmployeeName, req.EmployeeNum)
// 根据任务类型,要求该类型仍有可完成的余量
switch req.TaskType {
case "video":
query = query.Where("pending_video_count > 0 AND complete_video_count < assign_video_count")
case "post":
query = query.Where("pending_post_count > 0 AND complete_post_count < assign_post_count")
case "data":
query = query.Where("pending_data_count > 0 AND complete_data_count < assign_data_count")
case "script":
query = query.Where("pending_video_script_count > 0 AND complete_video_script_count < assign_video_script_count")
default:
tx.Rollback()
return commonErr.ReturnError(nil, "无效的任务类型", "任务类型必须是视频、图文、数据或脚本")
}
err = query.
Order("operator_time ASC").
First(&assignRecord).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
tx.Rollback()
return nil
}
tx.Rollback()
fmt.Println("查找指派记录失败")
return nil
}
}
// 2. 根据任务类型更新完成数量
updateData := map[string]interface{}{
"updated_at": time.Now(),
}
switch req.TaskType {
case "video":
newCompleteCount := assignRecord.CompleteVideoCount + req.CompleteCount
if newCompleteCount > assignRecord.AssignVideoCount {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("视频完成数量超出限制,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentCompleteCount", assignRecord.CompleteVideoCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newCompleteCount", newCompleteCount),
zap.Int("assignVideoCount", assignRecord.AssignVideoCount),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "完成数量超出指派数量", "视频完成数量不能超过已指派数量")
}
}
newPending := assignRecord.PendingVideoCount - req.CompleteCount
if newPending < 0 {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("待发视频数不足,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentPendingCount", assignRecord.PendingVideoCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newPendingCount", newPending),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "待发视频数不足", "待发视频数不能小于0")
}
}
updateData["complete_video_count"] = newCompleteCount
updateData["pending_video_count"] = newPending
case "post":
newCompleteCount := assignRecord.CompletePostCount + req.CompleteCount
if newCompleteCount > assignRecord.AssignPostCount {
// 暂时先只记录,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("图文完成数量超出限制,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentCompleteCount", assignRecord.CompletePostCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newCompleteCount", newCompleteCount),
zap.Int("assignPostCount", assignRecord.AssignPostCount),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "完成数量超出指派数量", "图文完成数量不能超过已指派数量")
}
}
newPending := assignRecord.PendingPostCount - req.CompleteCount
if newPending < 0 {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("待发图文数不足,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentPendingCount", assignRecord.PendingPostCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newPendingCount", newPending),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "待发图文数不足", "待发图文数不能小于0")
}
}
updateData["complete_post_count"] = newCompleteCount
updateData["pending_post_count"] = newPending
case "data":
newCompleteCount := assignRecord.CompleteDataCount + req.CompleteCount
if newCompleteCount > assignRecord.AssignDataCount {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("数据完成数量超出限制,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentCompleteCount", assignRecord.CompleteDataCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newCompleteCount", newCompleteCount),
zap.Int("assignDataCount", assignRecord.AssignDataCount),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "完成数量超出指派数量", "数据完成数量不能超过已指派数量")
}
}
newPending := assignRecord.PendingDataCount - req.CompleteCount
if newPending < 0 {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("待发数据数不足,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentPendingCount", assignRecord.PendingDataCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newPendingCount", newPending),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "待发数据数不足", "待发数据数不能小于0")
}
}
updateData["complete_data_count"] = newCompleteCount
updateData["pending_data_count"] = newPending
case "script":
newCompleteCount := assignRecord.CompleteVideoScriptCount + req.CompleteCount
if newCompleteCount > assignRecord.AssignVideoScriptCount {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("数据完成数量超出限制,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentCompleteCount", assignRecord.CompleteDataCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newCompleteCount", newCompleteCount),
zap.Int("assignDataCount", assignRecord.AssignDataCount),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "完成数量超出指派数量", "数据完成数量不能超过已指派数量")
}
}
newPending := assignRecord.PendingVideoScriptCount - req.CompleteCount
if newPending < 0 {
// 暂时都只记录错误,不报错;并且不更新
if req.AssignRecordsUUID != "false" {
app.ModuleClients.Lg.Info("待发数据数不足,跳过更新",
zap.String("assignRecordsUUID", assignRecord.AssignRecordsUUID),
zap.String("employeeName", req.EmployeeName),
zap.String("employeeNum", req.EmployeeNum),
zap.Int("currentPendingCount", assignRecord.PendingDataCount),
zap.Int("requestCompleteCount", req.CompleteCount),
zap.Int("newPendingCount", newPending),
)
tx.Rollback()
return nil
} else {
tx.Rollback()
return commonErr.ReturnError(nil, "待发数据数不足", "待发数据数不能小于0")
}
}
updateData["complete_video_script_count"] = newCompleteCount
updateData["pending_video_script_count"] = newPending
default:
tx.Rollback()
return commonErr.ReturnError(nil, "无效的任务类型", "任务类型必须是视频、图文、数据或脚本")
}
// 3. 更新指派记录
if err = tx.Model(&assignRecord).Updates(updateData).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新完成数量失败", "更新完成数量失败: ")
}
// 4. 重新查询更新后的记录,检查是否全部完成
if err = tx.Where("assign_records_uuid = ?", assignRecord.AssignRecordsUUID).First(&assignRecord).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "查询更新后记录失败", "查询更新后记录失败: ")
}
// 5. 检查是否所有任务都已完成
if assignRecord.CompleteVideoCount == assignRecord.AssignVideoCount &&
assignRecord.CompletePostCount == assignRecord.AssignPostCount &&
assignRecord.CompleteDataCount == assignRecord.AssignDataCount {
// 更新实际完成状态
if err = tx.Model(&assignRecord).Update("actual_status", 2).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "更新实际完成状态失败", "更新实际完成状态失败: ")
}
}
// 6. 记录本次完成项的 UUID
var typeCode int
switch req.TaskType {
case "video":
typeCode = 1
case "post":
typeCode = 2
case "data":
typeCode = 3
case "script":
typeCode = 4
}
if req.UUID != "" {
uuidItem := &model.TaskAssignUUIDItems{
AssignRecordsUUID: assignRecord.AssignRecordsUUID,
Type: typeCode,
UUID: req.UUID,
}
if err = tx.Create(uuidItem).Error; err != nil {
tx.Rollback()
return commonErr.ReturnError(err, "插入任务UUID记录失败", "插入任务UUID记录失败: ")
}
}
// 提交事务
if err = tx.Commit().Error; err != nil {
return commonErr.ReturnError(err, "提交事务失败", "提交事务失败: ")
}
return nil
}
// GetAssignRecordByUUID 根据UUID查询指派记录
func GetAssignRecordByUUID(uuid string) (*model.TaskAssignRecords, error) {
var record model.TaskAssignRecords
err := app.ModuleClients.TaskBenchDB.Where("assign_records_uuid = ?", uuid).First(&record).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil // 记录不存在
}
return nil, commonErr.ReturnError(err, "查询指派记录失败", "查询指派记录失败: ")
}
return &record, nil
}
// GetTaskAssignRecordsList 多条件查询操作记录表
func GetTaskAssignRecordsList(req *dto.TaskAssignRecordsQueryRequest) ([]*model.TaskAssignRecords, int64, *dto.TaskAssignRecordsSummary, error) {
var records []*model.TaskAssignRecords
var total int64
var summary dto.TaskAssignRecordsSummary
// 构建查询条件
query := app.ModuleClients.TaskBenchDB.Model(&model.TaskAssignRecords{}).
Select("task_assign_records.*, rn.name AS artist_name").
Joins("LEFT JOIN `micro-account`.`user` u ON u.sub_num COLLATE utf8mb4_general_ci = task_assign_records.sub_num COLLATE utf8mb4_general_ci").
Joins("LEFT JOIN `micro-account`.real_name rn ON u.real_name_id = rn.id").
Where("u.deleted_at = 0")
// 关键词搜索(艺人姓名、编号、手机号)
if req.Keyword != "" {
query = query.Where("task_assign_records.sub_num LIKE ? OR task_assign_records.tel_num LIKE ? OR rn.name LIKE ?",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 指派人姓名
if req.TaskAssignee != "" {
query = query.Where("task_assign_records.task_assignee LIKE ?", "%"+req.TaskAssignee+"%")
}
// 操作人姓名或手机号
if req.Operator != "" {
query = query.Where("task_assign_records.operator LIKE ? OR task_assign_records.operator_num LIKE ?", "%"+req.Operator+"%", "%"+req.Operator+"%")
}
// 操作人手机号
if req.OperatorNum != "" {
query = query.Where("task_assign_records.operator_num LIKE ?", "%"+req.OperatorNum+"%")
}
// 任务批次
if req.TaskBatch != "" {
query = query.Where("task_assign_records.task_batch LIKE ?", "%"+req.TaskBatch+"%")
}
// 操作时间区间
if req.StartTime != "" && req.EndTime != "" {
query = query.Where("task_assign_records.operator_time BETWEEN ? AND ?", req.StartTime, req.EndTime)
} else if req.StartTime != "" {
query = query.Where("task_assign_records.operator_time >= ?", req.StartTime)
} else if req.EndTime != "" {
query = query.Where("task_assign_records.operator_time <= ?", req.EndTime)
}
// 反馈完成状态
if req.Status != 0 {
query = query.Where("task_assign_records.status = ?", req.Status)
}
// 实际完成状态
if req.ActualStatus != 0 {
query = query.Where("task_assign_records.actual_status = ?", req.ActualStatus)
}
// 计算总数
query.Count(&total)
// 汇总:根据筛选条件分页前的所有艺人待发与已完成数量(同筛选条件,关联用户与实名表以支持按实名筛选)
sumQuery := app.ModuleClients.TaskBenchDB.Model(&model.TaskAssignRecords{}).
Joins("LEFT JOIN `micro-account`.`user` u ON u.sub_num COLLATE utf8mb4_general_ci = task_assign_records.sub_num COLLATE utf8mb4_general_ci").
Joins("LEFT JOIN `micro-account`.real_name rn ON u.real_name_id = rn.id").
Where("u.deleted_at = 0")
if req.Keyword != "" {
// 与列表查询保持一致:按艺人编号、手机号、实名表姓名进行模糊匹配
sumQuery = sumQuery.Where("task_assign_records.sub_num LIKE ? OR task_assign_records.tel_num LIKE ? OR rn.name LIKE ?",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.TaskAssignee != "" {
sumQuery = sumQuery.Where("task_assign_records.task_assignee LIKE ?", "%"+req.TaskAssignee+"%")
}
if req.Operator != "" {
sumQuery = sumQuery.Where("task_assign_records.operator LIKE ? OR task_assign_records.operator_num LIKE ?", "%"+req.Operator+"%", "%"+req.Operator+"%")
}
if req.OperatorNum != "" {
sumQuery = sumQuery.Where("task_assign_records.operator_num LIKE ?", "%"+req.OperatorNum+"%")
}
if req.TaskBatch != "" {
sumQuery = sumQuery.Where("task_assign_records.task_batch LIKE ?", "%"+req.TaskBatch+"%")
}
if req.StartTime != "" && req.EndTime != "" {
sumQuery = sumQuery.Where("task_assign_records.operator_time BETWEEN ? AND ?", req.StartTime, req.EndTime)
} else if req.StartTime != "" {
sumQuery = sumQuery.Where("task_assign_records.operator_time >= ?", req.StartTime)
} else if req.EndTime != "" {
sumQuery = sumQuery.Where("task_assign_records.operator_time <= ?", req.EndTime)
}
if req.Status != 0 {
sumQuery = sumQuery.Where("task_assign_records.status = ?", req.Status)
}
if req.ActualStatus != 0 {
sumQuery = sumQuery.Where("task_assign_records.actual_status = ?", req.ActualStatus)
}
// 执行汇总查询(不分页)
errSum := sumQuery.Select("\n SUM(task_assign_records.assign_video_script_count) AS total_pending_video_script_count,\n SUM(task_assign_records.assign_video_count) AS total_pending_video_count,\n SUM(task_assign_records.assign_post_count) AS total_pending_post_count,\n SUM(task_assign_records.assign_data_count) AS total_pending_data_count,\n SUM(task_assign_records.complete_video_script_count) AS total_complete_video_script_count,\n SUM(task_assign_records.complete_video_count) AS total_complete_video_count,\n SUM(task_assign_records.complete_post_count) AS total_complete_post_count,\n SUM(task_assign_records.complete_data_count) AS total_complete_data_count\n ").Scan(&summary).Error
if errSum != nil {
return nil, 0, nil, commonErr.ReturnError(errSum, "查询操作记录汇总失败", "查询操作记录汇总失败: ")
}
// 分页
if req.PageSize > 0 && req.Page > 0 {
offset := (req.Page - 1) * req.PageSize
query = query.Limit(req.PageSize).Offset(offset)
}
// 排序处理:白名单列 + sortType
// 允许的排序字段避免SQL注入
allowed := map[string]bool{
"operator_time": true,
"updated_at": true,
"complete_time": true,
"status": true,
"actual_status": true,
"task_batch": true,
"artist_name": true,
"sub_num": true,
"tel_num": true,
"task_assignee": true,
"operator": true,
"operator_num": true,
"task_assignee_num": true,
"pending_video_count": true,
"pending_post_count": true,
"pending_data_count": true,
"pending_video_script_count": true,
"assign_video_count": true,
"assign_post_count": true,
"assign_data_count": true,
"assign_video_script_count": true,
"complete_video_count": true,
"complete_post_count": true,
"complete_data_count": true,
"complete_video_script_count": true,
"created_at": true,
"assign_records_uuid": true,
}
// 默认按操作时间降序
orderClause := "operator_time DESC"
if req.SortBy != "" && allowed[req.SortBy] {
orderClause = fmt.Sprintf("%s %s", req.SortBy, req.SortType)
}
// 应用排序
err := query.Order(orderClause).Find(&records).Error
if err != nil {
return nil, 0, nil, commonErr.ReturnError(err, "查询操作记录失败", "查询操作记录失败: ")
}
return records, total, &summary, nil
}
// GetValidArtistList 查询套餐状态为有效中的艺人数据列表
func GetValidArtistList() ([]dto.ValidArtistInfo, error) {
// 构建子查询,获取每个用户的最新订单记录
subQuery := app.ModuleClients.BundleDB.Table("bundle_order_records as bor1").
Select("bor1.*").
Joins(`INNER JOIN (
SELECT customer_id, MAX(created_at) AS max_created_time
FROM bundle_order_records
GROUP BY customer_id
) bor2 ON bor1.customer_id = bor2.customer_id AND bor1.created_at = bor2.max_created_time`)
// 主查询,关联用户表和实名信息表
session := app.ModuleClients.BundleDB.Table("`micro-account`.`user` AS u").Unscoped().
Select(`u.id as user_id, bor.customer_num, rn.name as user_name,
u.tel_num as user_phone_number, bor.bundle_name, bor.expiration_time,
bor.status, bor.uuid as order_uuid,
(bb.bundle_account_number + bb.increase_account_number + bb.manual_account_number) as account_number,
(bb.bundle_account_consumption_number + bb.increase_account_consumption_number + bb.manual_account_consumption_number) as account_consumption_number,
(bb.bundle_video_number + bb.increase_video_number + bb.manual_video_number) as video_number,
(bb.bundle_video_consumption_number + bb.increase_video_consumption_number) as video_consumption_number,
(bb.bundle_image_number + bb.increase_image_number + bb.manual_image_number) as image_number,
(bb.bundle_image_consumption_number + bb.increase_image_consumption_number + bb.manual_image_consumption_number) as image_consumption_number,
(bb.bundle_data_analysis_number + bb.increase_data_analysis_number + bb.manual_data_analysis_number) as data_analysis_number,
(bb.bundle_data_analysis_consumption_number + bb.increase_data_analysis_consumption_number + bb.manual_data_analysis_consumption_number) as data_analysis_consumption_number,
bb.expansion_packs_number`).
Joins("LEFT JOIN `micro-account`.real_name rn ON u.real_name_id = rn.id").
Joins("LEFT JOIN (?) as bor ON bor.customer_id = u.id", subQuery).
Joins("LEFT JOIN bundle_balance bb ON u.id = bb.user_id AND bb.order_uuid COLLATE utf8mb4_general_ci = bor.uuid COLLATE utf8mb4_general_ci").
Joins("LEFT JOIN bundle_activate bc ON bc.user_id = u.id").
Where("rn.name IS NOT NULL").
Where("u.deleted_at = 0").
Where("bor.deleted_at IS NULL").
Where("bb.expired_at > ?", time.Now()).
Where("bc.activate = ?", 2).
Where("bb.month = ?", time.Now().Format("2006-01")).
Order("bor.expiration_time desc")
var data []dto.ValidArtistInfo
err := session.Find(&data).Error
if err != nil {
return nil, commonErr.ReturnError(err, "查询有效艺人失败", "查询有效艺人失败: ")
}
fmt.Println(data)
return data, nil
}
// 根据艺人编号统计进行中的与已完成任务数量
func GetArtistTaskStatsBySubNum(SubNum string) (int, int, error) {
if SubNum == "" {
return 0, 0, nil
}
var res struct {
ProgressTaskCount int
CompleteTaskCount int
}
err := app.ModuleClients.TaskBenchDB.Table("task_assign_records").
Select("COUNT(CASE WHEN status = 1 THEN 1 END) as progress_task_count, COUNT(CASE WHEN status = 2 THEN 1 END) as complete_task_count").
Where("sub_num = ? AND deleted_at = 0", SubNum).
Scan(&res).Error
if err != nil {
return 0, 0, nil
}
return res.ProgressTaskCount, res.CompleteTaskCount, nil
}
// CreateTaskWorkLog 创建任务日志记录
func CreateTaskWorkLog(req *dto.CreateTaskWorkLogRequest) error {
// 参数校验
if req == nil {
return commonErr.ReturnError(nil, "参数错误", "请求参数不能为空")
}
// 生成日志UUID
workLogUUID := uuid.New().String()
// 获取当前时间戳Unix时间戳秒级
now := int(time.Now().Unix())
// 构建日志记录
workLog := &model.TaskWorkLog{
WorkLogUUID: workLogUUID,
AssignRecordsUUID: req.AssignRecordsUUID,
WorkUUID: req.WorkUUID,
Title: req.Title,
ArtistUUID: req.ArtistUUID,
SubNum: req.SubNum,
TelNum: req.TelNum,
ArtistName: req.ArtistName,
OperationType: req.OperationType,
TaskType: req.TaskType,
TaskCount: req.TaskCount,
Remark: req.Remark,
OperatorName: req.OperatorName,
OperatorNum: req.OperatorNum,
OperationTime: now,
CreatedAt: now,
UpdatedAt: now,
}
// 插入数据库
if err := app.ModuleClients.TaskBenchDB.Create(workLog).Error; err != nil {
return commonErr.ReturnError(err, "创建任务日志失败", "创建任务日志失败: ")
}
return nil
}