Files
WorkBuddy/cnc-sales-backend/internal/api/tasks.go
T
2026-08-16 16:29:06 +08:00

276 lines
7.7 KiB
Go

package api
import (
"database/sql"
"net/http"
"strings"
"time"
"cnc-sales-backend/internal/auth"
"cnc-sales-backend/internal/models"
)
const taskCols = `t.id, t.title, t.customer_id, t.due_date, t.priority, t.status,
t.repeat, t.created_at, COALESCE(c.company,'')`
func (s *Server) loadTask(uid int64, id string) (*models.Task, error) {
t := &models.Task{}
var customerID, repeat sql.NullString
err := s.db.QueryRow(`SELECT `+taskCols+` FROM tasks t
LEFT JOIN customers c ON c.id=t.customer_id
WHERE t.owner_id=? AND t.id=? AND t.deleted_at IS NULL`, uid, id).
Scan(&t.ID, &t.Title, &customerID, &t.DueDate, &t.Priority, &t.Status,
&repeat, &t.CreatedAt, &t.CustomerName)
if err != nil {
return nil, err
}
if customerID.Valid && customerID.String != "" {
t.CustomerID = &customerID.String
}
if repeat.Valid && repeat.String != "" {
t.Repeat = &repeat.String
}
return t, nil
}
// listTasks 任务列表(支持 status / priority 筛选)
func (s *Server) listTasks(w http.ResponseWriter, r *http.Request) {
uid := auth.UserID(r)
query := `SELECT ` + taskCols + ` FROM tasks t
LEFT JOIN customers c ON c.id=t.customer_id
WHERE t.owner_id=? AND t.deleted_at IS NULL`
args := []any{uid}
if v := r.URL.Query().Get("status"); v != "" {
query += " AND t.status=?"
args = append(args, models.NormalizeStatus(v))
}
if v := r.URL.Query().Get("priority"); v != "" {
query += " AND t.priority=?"
args = append(args, models.NormalizePriority(v))
}
query += " ORDER BY CASE t.status WHEN 'done' THEN 1 ELSE 0 END, t.due_date, t.created_at DESC"
rows, err := s.db.Query(query, args...)
if err != nil {
writeErr(w, http.StatusInternalServerError, "查询失败")
return
}
defer rows.Close()
out := []models.Task{}
for rows.Next() {
t := models.Task{}
var customerID, repeat sql.NullString
if err := rows.Scan(&t.ID, &t.Title, &customerID, &t.DueDate, &t.Priority, &t.Status,
&repeat, &t.CreatedAt, &t.CustomerName); err != nil {
writeErr(w, http.StatusInternalServerError, "数据读取失败")
return
}
if customerID.Valid && customerID.String != "" {
t.CustomerID = &customerID.String
}
if repeat.Valid && repeat.String != "" {
t.Repeat = &repeat.String
}
out = append(out, t)
}
if err := rows.Err(); err != nil {
writeErr(w, http.StatusInternalServerError, "数据读取失败")
return
}
writeJSON(w, http.StatusOK, out)
}
// createTask 新建任务
func (s *Server) createTask(w http.ResponseWriter, r *http.Request) {
uid := auth.UserID(r)
var t models.Task
if err := decodeJSON(r, &t); err != nil {
writeErr(w, http.StatusBadRequest, "请求格式错误")
return
}
if strings.TrimSpace(t.Title) == "" {
writeErr(w, http.StatusBadRequest, "任务标题必填")
return
}
if t.ID == "" {
t.ID = genID()
}
t.Priority = models.NormalizePriority(t.Priority)
t.Status = models.NormalizeStatus(t.Status)
t.Repeat = models.NormalizeRepeat(deref(t.Repeat))
if t.CreatedAt == "" {
t.CreatedAt = nowStr()
}
customerID := nullOrEmpty(t.CustomerID)
_, err := s.db.Exec(`INSERT INTO tasks (id, owner_id, title, customer_id, due_date, priority,
status, repeat, created_at) VALUES (?,?,?,?,?,?,?,?,?)`,
t.ID, uid, t.Title, customerID, t.DueDate, t.Priority, t.Status, nullOrEmpty(t.Repeat), t.CreatedAt)
if err != nil {
writeErr(w, http.StatusInternalServerError, "保存失败")
return
}
full, err := s.loadTask(uid, t.ID)
if err != nil {
writeErr(w, http.StatusInternalServerError, "读取失败")
return
}
writeJSON(w, http.StatusCreated, full)
}
// updateTask 更新任务
func (s *Server) updateTask(w http.ResponseWriter, r *http.Request) {
uid := auth.UserID(r)
id := pathID(r, "id")
if _, err := s.loadTask(uid, id); err != nil {
writeErr(w, http.StatusNotFound, "任务不存在")
return
}
var t models.Task
if err := decodeJSON(r, &t); err != nil {
writeErr(w, http.StatusBadRequest, "请求格式错误")
return
}
if strings.TrimSpace(t.Title) == "" {
writeErr(w, http.StatusBadRequest, "任务标题必填")
return
}
t.Priority = models.NormalizePriority(t.Priority)
t.Status = models.NormalizeStatus(t.Status)
t.Repeat = models.NormalizeRepeat(deref(t.Repeat))
completedAt := any(nil)
if t.Status == "done" {
completedAt = nowStr()
}
_, err := s.db.Exec(`UPDATE tasks SET title=?, customer_id=?, due_date=?, priority=?, status=?,
repeat=?, completed_at=? WHERE id=? AND owner_id=?`,
t.Title, nullOrEmpty(t.CustomerID), t.DueDate, t.Priority, t.Status,
nullOrEmpty(t.Repeat), completedAt, id, uid)
if err != nil {
writeErr(w, http.StatusInternalServerError, "保存失败")
return
}
full, err := s.loadTask(uid, id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "读取失败")
return
}
writeJSON(w, http.StatusOK, full)
}
// deleteTask 软删除任务
func (s *Server) deleteTask(w http.ResponseWriter, r *http.Request) {
uid := auth.UserID(r)
id := pathID(r, "id")
res, err := s.db.Exec("UPDATE tasks SET deleted_at=? WHERE id=? AND owner_id=? AND deleted_at IS NULL",
nowStr(), id, uid)
if err != nil {
writeErr(w, http.StatusInternalServerError, "删除失败")
return
}
if n, _ := res.RowsAffected(); n == 0 {
writeErr(w, http.StatusNotFound, "任务不存在")
return
}
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
// toggleTask 完成/取消任务;完成时若带重复规则则自动生成下一周期任务
func (s *Server) toggleTask(w http.ResponseWriter, r *http.Request) {
uid := auth.UserID(r)
id := pathID(r, "id")
cur, err := s.loadTask(uid, id)
if err != nil {
writeErr(w, http.StatusNotFound, "任务不存在")
return
}
var next *models.Task
if cur.Status == "done" {
// 取消完成
_, err = s.db.Exec("UPDATE tasks SET status='todo', completed_at=NULL WHERE id=? AND owner_id=?", id, uid)
if err != nil {
writeErr(w, http.StatusInternalServerError, "保存失败")
return
}
} else {
// 标记完成
_, err = s.db.Exec("UPDATE tasks SET status='done', completed_at=? WHERE id=? AND owner_id=?", nowStr(), id, uid)
if err != nil {
writeErr(w, http.StatusInternalServerError, "保存失败")
return
}
if cur.Repeat != nil {
next = s.genRepeatTask(uid, cur)
}
}
full, err := s.loadTask(uid, id)
if err != nil {
writeErr(w, http.StatusInternalServerError, "读取失败")
return
}
if next == nil {
writeJSON(w, http.StatusOK, full)
return
}
writeJSON(w, http.StatusOK, map[string]any{"task": full, "next": next})
}
// genRepeatTask 按重复规则生成下一周期任务
func (s *Server) genRepeatTask(uid int64, cur *models.Task) *models.Task {
base := time.Now()
if cur.DueDate != "" {
if d, err := time.Parse("2006-01-02", cur.DueDate); err == nil {
base = d
}
}
switch *cur.Repeat {
case "daily":
base = base.AddDate(0, 0, 1)
case "weekly":
base = base.AddDate(0, 0, 7)
case "monthly":
base = base.AddDate(0, 1, 0)
}
nt := &models.Task{
ID: genID(),
Title: cur.Title,
CustomerID: cur.CustomerID,
DueDate: base.Format("2006-01-02"),
Priority: cur.Priority,
Status: "todo",
Repeat: cur.Repeat,
CreatedAt: nowStr(),
}
_, err := s.db.Exec(`INSERT INTO tasks (id, owner_id, title, customer_id, due_date, priority,
status, repeat, created_at) VALUES (?,?,?,?,?,?,?,?,?)`,
nt.ID, uid, nt.Title, nullOrEmpty(nt.CustomerID), nt.DueDate, nt.Priority, nt.Status,
nullOrEmpty(nt.Repeat), nt.CreatedAt)
if err != nil {
return nil
}
if full, err := s.loadTask(uid, nt.ID); err == nil {
return full
}
return nt
}
// nullOrEmpty 空指针/空串转 NULL
func nullOrEmpty[T string](v *T) any {
if v == nil || strings.TrimSpace(string(*v)) == "" {
return nil
}
return *v
}
func deref[T any](v *T) T {
var zero T
if v == nil {
return zero
}
return *v
}