feat: 增加客户端采购员归属管理 (#54)

This commit is contained in:
chengma
2026-08-09 16:02:25 +08:00
parent 0181170457
commit e0c0dac7a0
17 changed files with 766 additions and 37 deletions
+76 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin"
"cmautobuy/admin/config"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
"cmautobuy/admin/service"
"cmautobuy/admin/syb"
@@ -535,8 +536,9 @@ func (h *Handler) taskRedirect(c *gin.Context, msg string) {
// (没有心跳是有意的,见 docs/admin/04-client-api.md §3)。
func (h *Handler) ClientList(c *gin.Context) {
keyword := c.Query("name")
actor := currentUser(c)
views, err := service.ListClientViews(h.db, keyword, h.onlineThreshold)
views, err := service.ListClientViewsForUser(h.db, actor, keyword, h.onlineThreshold)
if err != nil {
fail(c, http.StatusInternalServerError,
"读取客户端列表失败,数据没有被改动。请稍后重试,或查看 data/logs/ 里的日志。")
@@ -552,16 +554,85 @@ func (h *Handler) ClientList(c *gin.Context) {
status := fmt.Sprintf("共 %d 台客户端 · 在线 %d · 离线 %d",
len(views), online, len(views)-online)
if len(views) == 0 {
status = "还没有客户端。客户端第一次调用领取接口时会自动登记。"
if actor != nil && actor.IsAdmin() {
status = "还没有客户端。客户端第一次调用领取接口时会自动登记。"
} else {
status = "当前没有绑定给你的客户端,请联系管理员。"
}
}
purchasers := []model.User{}
if actor != nil && actor.IsAdmin() {
purchasers, err = service.ListActivePurchasers(h.db, actor)
if err != nil {
fail(c, http.StatusInternalServerError,
"读取可绑定采购员失败,数据没有被改动。请稍后重试。")
return
}
}
c.HTML(http.StatusOK, "client/list", page(c, "clients", "客户端列表", gin.H{
"Keyword": keyword,
"Rows": views,
"Status": status,
"Keyword": keyword, "Rows": views, "Status": status,
"Purchasers": purchasers, "Message": c.Query("msg"), "Error": c.Query("error"),
}))
}
// ClientAssign 由管理员绑定或转交客户端。只改归属历史,不改 tasks。
func (h *Handler) ClientAssign(c *gin.Context) {
changed, transferred, err := service.AssignClient(h.db, currentUser(c),
c.PostForm("client_id"), c.PostForm("user_id"), time.Now())
if err != nil {
h.clientActionFailure(c, err, "绑定客户端失败")
return
}
message := "客户端已经绑定给该采购员,无需重复操作"
if changed {
message = "客户端绑定成功"
}
if transferred {
message = "客户端已转交,新负责人立即生效;既有任务保持不变"
}
log.Printf("client_assignment_changed actor=%s client_id=%s user_id=%s transferred=%t changed=%t",
currentUser(c).Username, c.PostForm("client_id"), c.PostForm("user_id"), transferred, changed)
redirectClients(c, message, "")
}
// ClientUnassign 由管理员解绑客户端。前端确认用于防误触,服务端权限仍是硬边界。
func (h *Handler) ClientUnassign(c *gin.Context) {
err := service.UnassignClient(h.db, currentUser(c), c.PostForm("client_id"), time.Now())
if err != nil {
h.clientActionFailure(c, err, "解绑客户端失败")
return
}
log.Printf("client_assignment_ended actor=%s client_id=%s",
currentUser(c).Username, c.PostForm("client_id"))
redirectClients(c, "客户端已解绑;归属历史和既有任务均已保留", "")
}
func (h *Handler) clientActionFailure(c *gin.Context, err error, fallback string) {
if service.IsValidationError(err) || errors.Is(err, repository.ErrClientNotFound) ||
errors.Is(err, repository.ErrPurchaserNotActive) || errors.Is(err, repository.ErrClientNotAssigned) {
redirectClients(c, "", err.Error())
return
}
fail(c, http.StatusInternalServerError, fallback+",已有归属和任务数据保持不变。刷新后重试。")
}
func redirectClients(c *gin.Context, message, errorMessage string) {
query := url.Values{}
if message != "" {
query.Set("msg", message)
}
if errorMessage != "" {
query.Set("error", errorMessage)
}
target := "/clients"
if encoded := query.Encode(); encoded != "" {
target += "?" + encoded
}
c.Redirect(http.StatusSeeOther, target)
}
// ClientDelete 批量删除客户端。
//
// 二次确认在前端做(见 static/js/app.js),这里直接删。
+2 -2
View File
@@ -15,7 +15,7 @@ import (
"cmautobuy/admin/service"
)
// AdminRequired 叠加在已认证的 /users 路由上。采购员能使用业务页,但不能管理账号。
// AdminRequired 叠加在已认证路由上。采购员能使用业务页,但不能执行管理操作。
func AdminRequired() gin.HandlerFunc {
return func(c *gin.Context) {
user := currentUser(c)
@@ -23,7 +23,7 @@ func AdminRequired() gin.HandlerFunc {
c.Next()
return
}
fail(c, http.StatusForbidden, "只有管理员可以访问用户管理。当前账号仍可使用其他业务页面。")
fail(c, http.StatusForbidden, "只有管理员可以执行此管理操作。当前账号仍可使用其他业务页面。")
c.Abort()
}
}
+4 -1
View File
@@ -82,7 +82,10 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
// 5. 客户端列表
pages.GET("/clients", h.ClientList)
pages.POST("/clients/delete", h.ClientDelete)
clients := pages.Group("/clients", AdminRequired())
clients.POST("/assign", h.ClientAssign)
clients.POST("/unassign", h.ClientUnassign)
clients.POST("/delete", h.ClientDelete)
// 6. 用户管理:先经过网页登录,再叠加管理员角色校验。
users := pages.Group("/users", AdminRequired())