276 lines
7.7 KiB
Go
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
|
|
}
|