fix: add purchaser role permissions (#85)

This commit is contained in:
QiuSW
2026-08-25 14:25:22 +08:00
parent ab5832d00f
commit ae9a9fef6f
18 changed files with 400 additions and 23 deletions
+12 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Business-Rules-and-Glossary
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
wiki_revision: 73e38d6204acee7b6ea2c2eb9e9c918e807dbf3a
synchronized_at: 2026-08-25T01:39:35Z
wiki_revision: 6bad462cbf91d592ba95fbe2003402d20a589cc3
synchronized_at: 2026-08-25T06:21:31Z
<!-- gitea-wiki-mirror:end -->
# 业务规则与术语
@@ -152,6 +152,16 @@ synchronized_at: 2026-08-25T01:39:35Z
- 只有正式采购、未进入不可逆边界、没有订单号或下单时间、且仍是同一 SYB 最新记录的 `failed` 任务可重试。原设备离线、停用、忙碌或能力不足时该项失败且不自动换机;未指定设备时仍由空闲设备领取。
- 批量重试逐项处理并允许部分成功;同一请求幂等重放不会重复创建。失败任务不再使用一次性重新采购授权,该授权只保留给已经创建过订单且满足条件的任务。
## 管理端角色与权限
- 管理端固定支持 `admin`(管理员)和 `purchaser`(采购员)两类业务角色;用户必须绑定一个存在且启用的角色,`role_id=0` 或停用角色不能创建或保存。
- 采购员可读取设备状态,维护 PDD/虾皮商品和规格映射,查看与修正 SYB 商品,读取 SYB 店铺及同步记录,读取采集规则,创建/重置/删除采集任务,并创建、查看、重试及人工处理采购任务。
- 仅管理员可管理用户、角色、菜单、接口、部门和岗位;停用设备或吊销 Device Token;新增、改名、启停、删除或发现 SYB 店铺;手动启动 SYB 同步;新增、编辑或删除采集规则;保存或测试 AI Provider 配置。
- 采购员只能读取 AI 匹配是否启用,不得读取 Provider 地址、模型、API Key 等敏感配置。
- GoAuto 管理接口使用 Casbin 按 HTTP 方法和路径授权;设备凭据、规则和外部服务配置等高风险写操作另有管理员角色守卫,前端隐藏入口不能替代服务端授权。
- 管理员继续使用 `admin` 旁路;采购员必须由明确的 `purchaser` 策略授权,不得通过赋予管理员角色临时解决登录或权限问题。
- 任何角色都不得执行自动支付。
## 自动化边界
- 浏览器“打开拼多多APP”和系统确认框“打开”属于允许动作。
+2 -2
View File
@@ -94,7 +94,7 @@ type SysUserInsertReq struct {
Password string `json:"password" comment:"密码"`
NickName string `json:"nickName" comment:"昵称" vd:"len($)>0"`
Phone string `json:"phone" comment:"手机号" vd:"len($)>0"`
RoleId int `json:"roleId" comment:"角色ID"`
RoleId int `json:"roleId" comment:"角色ID" vd:"$>0"`
Avatar string `json:"avatar" comment:"头像"`
Sex string `json:"sex" comment:"性别"`
Email string `json:"email" comment:"邮箱" vd:"len($)>0,email"`
@@ -133,7 +133,7 @@ type SysUserUpdateReq struct {
Username string `json:"username" comment:"用户名" vd:"len($)>0"`
NickName string `json:"nickName" comment:"昵称" vd:"len($)>0"`
Phone string `json:"phone" comment:"手机号" vd:"len($)>0"`
RoleId int `json:"roleId" comment:"角色ID"`
RoleId int `json:"roleId" comment:"角色ID" vd:"$>0"`
Avatar string `json:"avatar" comment:"头像"`
Sex string `json:"sex" comment:"性别"`
Email string `json:"email" comment:"邮箱" vd:"len($)>0,email"`
+21
View File
@@ -74,6 +74,9 @@ func (e *SysUser) Insert(c *dto.SysUserInsertReq) error {
e.Log.Errorf("db error: %s", err)
return err
}
if err = validateEnabledRole(e.Orm, c.RoleId); err != nil {
return err
}
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
@@ -98,6 +101,9 @@ func (e *SysUser) Update(c *dto.SysUserUpdateReq, p *actions.DataPermission) err
return errors.New("无权更新该数据")
}
if err = validateEnabledRole(e.Orm, c.RoleId); err != nil {
return err
}
c.Generate(&model)
update := e.Orm.Model(&model).Where("user_id = ?", &model.UserId).Omit("password", "salt").Updates(&model)
if err = update.Error; err != nil {
@@ -112,6 +118,21 @@ func (e *SysUser) Update(c *dto.SysUserUpdateReq, p *actions.DataPermission) err
return nil
}
func validateEnabledRole(db *gorm.DB, roleID int) error {
if roleID <= 0 {
return errors.New("请选择有效角色")
}
var count int64
if err := db.Model(&models.SysRole{}).
Where("role_id = ? AND status = ?", roleID, "2").Count(&count).Error; err != nil {
return err
}
if count != 1 {
return errors.New("所选角色不存在或已停用")
}
return nil
}
// UpdateAvatar 更新用户头像
func (e *SysUser) UpdateAvatar(c *dto.UpdateSysUserAvatarReq, p *actions.DataPermission) error {
var err error
@@ -0,0 +1,34 @@
package service
import (
"testing"
"go-admin/app/admin/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestValidateEnabledRole(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&models.SysRole{}); err != nil {
t.Fatal(err)
}
if err = db.Create(&models.SysRole{RoleName: "采购员", RoleKey: "purchaser", Status: "2"}).Error; err != nil {
t.Fatal(err)
}
var role models.SysRole
db.Where("role_key = ?", "purchaser").First(&role)
if err = validateEnabledRole(db, role.RoleId); err != nil {
t.Fatalf("enabled role rejected: %v", err)
}
if err = validateEnabledRole(db, 0); err == nil {
t.Fatal("role zero must be rejected")
}
if err = validateEnabledRole(db, role.RoleId+100); err == nil {
t.Fatal("missing role must be rejected")
}
}
+97
View File
@@ -0,0 +1,97 @@
package access
// RolePurchaser is the fixed role key used by the GoAuto purchaser account.
const RolePurchaser = "purchaser"
// APIPermission describes one admin API known to GoAuto. Purchaser marks the
// APIs that the purchaser role may call; every other API remains admin-only.
type APIPermission struct {
Title string
Path string
Method string
Purchaser bool
}
// AdminAPIs is the auditable permission matrix for GoAuto's authenticated
// admin surface. Agent APIs are intentionally excluded from management roles.
var AdminAPIs = []APIPermission{
{"查看设备", "/api/admin/v1/devices", "GET", true},
{"停用设备", "/api/admin/v1/devices/:deviceId/disable", "POST", false},
{"吊销设备令牌", "/api/admin/v1/devices/:deviceId/token/revoke", "POST", false},
{"查看 PDD 商品", "/api/admin/v1/pdd-products", "GET", true},
{"新增 PDD 商品", "/api/admin/v1/pdd-products", "POST", true},
{"查看 PDD 商品详情", "/api/admin/v1/pdd-products/:productId", "GET", true},
{"修改 PDD 商品", "/api/admin/v1/pdd-products/:productId", "PATCH", true},
{"查看虾皮商品", "/api/admin/v1/shopee-products", "GET", true},
{"新增虾皮商品", "/api/admin/v1/shopee-products", "POST", true},
{"批量删除虾皮商品", "/api/admin/v1/shopee-products/batch-delete", "POST", true},
{"查看虾皮商品详情", "/api/admin/v1/shopee-products/:productId", "GET", true},
{"修改虾皮商品", "/api/admin/v1/shopee-products/:productId", "PATCH", true},
{"关联 PDD 商品", "/api/admin/v1/shopee-products/:productId/link-pdd", "POST", true},
{"恢复虾皮商品", "/api/admin/v1/shopee-products/:productId/restore", "POST", true},
{"新增虾皮规格值", "/api/admin/v1/shopee-products/:productId/specs/values", "POST", true},
{"删除虾皮规格值", "/api/admin/v1/shopee-products/:productId/specs/values", "DELETE", true},
{"设置规格映射", "/api/admin/v1/shopee-products/:productId/specs/mapping", "PUT", true},
{"清除规格映射", "/api/admin/v1/shopee-products/:productId/specs/mapping", "DELETE", true},
{"确认规格映射", "/api/admin/v1/shopee-products/:productId/specs/mapping/confirm", "POST", true},
{"确认精确规格映射", "/api/admin/v1/shopee-products/:productId/specs/mapping/confirm-exact-matches", "POST", true},
{"预览尺码匹配", "/api/admin/v1/shopee-products/:productId/specs/mapping/preview-auto-size", "POST", true},
{"查看 SYB 商品", "/api/admin/v1/syb-products", "GET", true},
{"查看 SYB 商品详情", "/api/admin/v1/syb-products/:productId", "GET", true},
{"重新解析 SYB 商品", "/api/admin/v1/syb-products/:productId/reparse", "POST", true},
{"批量重新解析 SYB 商品", "/api/admin/v1/syb-products/reparse-batch", "POST", true},
{"人工修正 SYB 商品", "/api/admin/v1/syb-products/:productId/correction", "PATCH", true},
{"手动同步 SYB 商品", "/api/admin/v1/syb-products/import", "POST", false},
{"查看 SYB 同步记录", "/api/admin/v1/syb-products/sync-runs", "GET", true},
{"查看 SYB 同步详情", "/api/admin/v1/syb-products/sync-runs/:runId", "GET", true},
{"查看 SYB 店铺", "/api/admin/v1/syb-shops", "GET", true},
{"新增 SYB 店铺", "/api/admin/v1/syb-shops", "POST", false},
{"修改 SYB 店铺名称", "/api/admin/v1/syb-shops/:shopId/name", "PATCH", false},
{"启停 SYB 店铺", "/api/admin/v1/syb-shops/:shopId/enabled", "PATCH", false},
{"删除 SYB 店铺", "/api/admin/v1/syb-shops/:shopId", "DELETE", false},
{"从 SYB 发现店铺", "/api/admin/v1/syb-shops/discover", "POST", false},
{"查看采集规则模板", "/api/admin/v1/collection-rules/templates/:templateId", "GET", true},
{"查看采集规则", "/api/admin/v1/collection-rules", "GET", true},
{"新增采集规则", "/api/admin/v1/collection-rules", "POST", false},
{"修改采集规则", "/api/admin/v1/collection-rules/:ruleId", "PATCH", false},
{"删除采集规则", "/api/admin/v1/collection-rules/:ruleId", "DELETE", false},
{"查看采集任务", "/api/admin/v1/collection-tasks", "GET", true},
{"创建采集任务", "/api/admin/v1/collection-tasks", "POST", true},
{"批量创建采集任务", "/api/admin/v1/collection-tasks/batch", "POST", true},
{"查看采集任务详情", "/api/admin/v1/collection-tasks/:taskId", "GET", true},
{"重置采集任务", "/api/admin/v1/collection-tasks/:taskId/reset", "POST", true},
{"删除采集任务", "/api/admin/v1/collection-tasks/:taskId", "DELETE", true},
{"查看采购任务", "/api/admin/v1/purchase-tasks", "GET", true},
{"预检批量采购", "/api/admin/v1/purchase-tasks/batch-preview", "POST", true},
{"批量创建采购任务", "/api/admin/v1/purchase-tasks/batch", "POST", true},
{"批量重试采购任务", "/api/admin/v1/purchase-tasks/batch-retry", "POST", true},
{"查看采购任务详情", "/api/admin/v1/purchase-tasks/:taskId", "GET", true},
{"创建采购任务", "/api/admin/v1/purchase-tasks", "POST", true},
{"处理采购规格", "/api/admin/v1/purchase-tasks/:taskId/spec-decision", "POST", true},
{"授权重新采购", "/api/admin/v1/purchase-tasks/:taskId/authorize-repurchase", "POST", true},
{"复核支付状态", "/api/admin/v1/purchase-tasks/:taskId/payment-review", "POST", true},
{"选择回填候选", "/api/admin/v1/purchase-tasks/:taskId/writeback-candidate", "POST", true},
{"取消采购任务", "/api/admin/v1/purchase-tasks/:taskId/cancel", "POST", true},
{"处理结果不明确任务", "/api/admin/v1/purchase-tasks/:taskId/resolve-unknown", "POST", true},
{"查看 AI 匹配状态", "/api/admin/v1/ai-matching-settings", "GET", true},
{"保存 AI 匹配设置", "/api/admin/v1/ai-matching-settings", "PUT", false},
{"测试 AI 服务连接", "/api/admin/v1/ai-matching-settings/test", "POST", false},
}
func PurchaserAPIs() []APIPermission {
result := make([]APIPermission, 0, len(AdminAPIs))
for _, permission := range AdminAPIs {
if permission.Purchaser {
result = append(result, permission)
}
}
return result
}
@@ -0,0 +1,28 @@
package access
import "testing"
func TestPurchaserPermissionMatrixHasNoDuplicates(t *testing.T) {
seen := map[string]bool{}
for _, permission := range AdminAPIs {
key := permission.Method + " " + permission.Path
if seen[key] {
t.Fatalf("duplicate API permission: %s", key)
}
seen[key] = true
}
}
func TestPurchaserExcludesAdministratorOperations(t *testing.T) {
denied := map[string]bool{
"POST /api/admin/v1/devices/:deviceId/disable": true,
"POST /api/admin/v1/syb-products/import": true,
"POST /api/admin/v1/collection-rules": true,
"PUT /api/admin/v1/ai-matching-settings": true,
}
for _, permission := range PurchaserAPIs() {
if denied[permission.Method+" "+permission.Path] {
t.Fatalf("administrator-only API granted to purchaser: %s %s", permission.Method, permission.Path)
}
}
}
+2 -2
View File
@@ -21,6 +21,6 @@ func InitRouter(engine *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) {
admin := engine.Group("/api/admin/v1/devices").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
admin.GET("", handler.List)
admin.POST("/:deviceId/disable", handler.Disable)
admin.POST("/:deviceId/token/revoke", handler.RevokeToken)
admin.POST("/:deviceId/disable", middleware.RequireRoleKey("admin"), handler.Disable)
admin.POST("/:deviceId/token/revoke", middleware.RequireRoleKey("admin"), handler.RevokeToken)
}
+3 -3
View File
@@ -11,7 +11,7 @@ func InitRouter(engine *gin.Engine, auth *jwt.GinJWTMiddleware) {
admin := engine.Group("/api/admin/v1/collection-rules").Use(auth.MiddlewareFunc()).Use(middleware.AuthCheckRole())
admin.GET("/templates/:templateId", handler.Template)
admin.GET("", handler.List)
admin.POST("", handler.Create)
admin.PATCH("/:ruleId", handler.Update)
admin.DELETE("/:ruleId", handler.Delete)
admin.POST("", middleware.RequireRoleKey("admin"), handler.Create)
admin.PATCH("/:ruleId", middleware.RequireRoleKey("admin"), handler.Update)
admin.DELETE("/:ruleId", middleware.RequireRoleKey("admin"), handler.Delete)
}
+1 -1
View File
@@ -27,6 +27,6 @@ func InitRouter(engine *gin.Engine, auth *jwt.GinJWTMiddleware) {
// 「从 SYB 发现店铺」挂在店铺路径下,但实现放在本包:它需要 SYB 客户端,
// 而本包已经依赖 sybshop 做导入过滤,反过来会形成包循环。
engine.Group("/api/admin/v1/syb-shops").Use(auth.MiddlewareFunc()).Use(middleware.AuthCheckRole()).
POST("/discover", handler.Discover)
POST("/discover", middleware.RequireRoleKey("admin"), handler.Discover)
admin.PATCH("/:productId/correction", handler.ManualCorrect)
}
+4 -4
View File
@@ -23,8 +23,8 @@ func InitRouter(engine *gin.Engine, auth *jwt.GinJWTMiddleware) {
group.GET("", handler.List)
// 配置:仅管理员(授权在系统管理里配置)
group.POST("", handler.Create)
group.PATCH("/:shopId/name", handler.Rename)
group.PATCH("/:shopId/enabled", handler.SetEnabled)
group.DELETE("/:shopId", handler.Delete)
group.POST("", middleware.RequireRoleKey("admin"), handler.Create)
group.PATCH("/:shopId/name", middleware.RequireRoleKey("admin"), handler.Rename)
group.PATCH("/:shopId/enabled", middleware.RequireRoleKey("admin"), handler.SetEnabled)
group.DELETE("/:shopId", middleware.RequireRoleKey("admin"), handler.Delete)
}
@@ -0,0 +1,78 @@
package version_local
import (
"runtime"
"go-admin/app/goauto/access"
"go-admin/cmd/migrate/migration"
migrationmodels "go-admin/cmd/migrate/migration/models"
common "go-admin/common/models"
"gorm.io/gorm"
)
func init() {
_, fileName, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(fileName), migratePurchaserRole)
}
func migratePurchaserRole(db *gorm.DB, version string) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := ensurePurchaserRoleAndPolicies(tx); err != nil {
return err
}
return tx.Create(&common.Migration{Version: version}).Error
})
}
// purchaserCasbinRule deliberately targets the table used by gorm-adapter.
// The inherited go-admin migration model points at an unused
// `sys_casbin_rule` table and must not be used for runtime authorization.
type purchaserCasbinRule struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:100"`
V0 string `gorm:"size:100"`
V1 string `gorm:"size:100"`
V2 string `gorm:"size:100"`
V3 string `gorm:"size:100"`
V4 string `gorm:"size:100"`
V5 string `gorm:"size:100"`
}
func (purchaserCasbinRule) TableName() string { return "casbin_rule" }
func ensurePurchaserRoleAndPolicies(db *gorm.DB) error {
role := migrationmodels.SysRole{}
if err := db.Where("role_key = ?", access.RolePurchaser).
Assign(migrationmodels.SysRole{
RoleName: "采购员", Status: "2", RoleSort: 20, Admin: false,
DataScope: "1", Remark: "GoAuto 采购业务角色(系统维护)",
}).FirstOrCreate(&role, migrationmodels.SysRole{RoleKey: access.RolePurchaser}).Error; err != nil {
return err
}
for _, permission := range access.AdminAPIs {
api := migrationmodels.SysApi{}
if err := db.Where("path = ? AND action = ?", permission.Path, permission.Method).
Attrs(migrationmodels.SysApi{Title: permission.Title, Type: "BUS"}).
FirstOrCreate(&api).Error; err != nil {
return err
}
}
// The role is system-maintained: reconcile its policies to the reviewed
// matrix so stale grants cannot survive a permission reduction.
if err := db.Where("ptype = ? AND v0 = ?", "p", access.RolePurchaser).
Delete(&purchaserCasbinRule{}).Error; err != nil {
return err
}
for _, permission := range access.PurchaserAPIs() {
rule := purchaserCasbinRule{
Ptype: "p", V0: access.RolePurchaser, V1: permission.Path, V2: permission.Method,
}
if err := db.Create(&rule).Error; err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,48 @@
package version_local
import (
"testing"
"go-admin/app/goauto/access"
migrationmodels "go-admin/cmd/migrate/migration/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestEnsurePurchaserRoleAndPolicies(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&migrationmodels.SysRole{}, &migrationmodels.SysApi{}, &purchaserCasbinRule{}); err != nil {
t.Fatal(err)
}
if err = ensurePurchaserRoleAndPolicies(db); err != nil {
t.Fatal(err)
}
if err = ensurePurchaserRoleAndPolicies(db); err != nil {
t.Fatalf("reconcile must be repeatable: %v", err)
}
var role migrationmodels.SysRole
if err = db.Where("role_key = ?", access.RolePurchaser).First(&role).Error; err != nil {
t.Fatal(err)
}
if role.RoleName != "采购员" || role.Status != "2" || role.Admin {
t.Fatalf("unexpected purchaser role: %#v", role)
}
var count int64
db.Model(&purchaserCasbinRule{}).Where("ptype = ? AND v0 = ?", "p", access.RolePurchaser).Count(&count)
if count != int64(len(access.PurchaserAPIs())) {
t.Fatalf("got %d policies, want %d", count, len(access.PurchaserAPIs()))
}
var forbidden int64
db.Model(&purchaserCasbinRule{}).
Where("v0 = ? AND v1 = ? AND v2 = ?", access.RolePurchaser, "/api/admin/v1/syb-products/import", "POST").
Count(&forbidden)
if forbidden != 0 {
t.Fatal("manual SYB import must remain administrator-only")
}
}
+26
View File
@@ -0,0 +1,26 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
)
// RequireRoleKey adds defense in depth for administrator-only operations.
// Casbin remains the primary API permission layer; this guard prevents a
// future policy/configuration mistake from widening a sensitive operation.
func RequireRoleKey(roleKey string) gin.HandlerFunc {
return func(c *gin.Context) {
role, _ := jwt.ExtractClaims(c)["rolekey"].(string)
if role != roleKey {
c.JSON(http.StatusForbidden, gin.H{
"code": "FORBIDDEN",
"message": "只有管理员可以执行此操作",
})
c.Abort()
return
}
c.Next()
}
}
+31
View File
@@ -0,0 +1,31 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
)
func TestRequireRoleKey(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, test := range []struct {
role string
want int
}{{role: "admin", want: http.StatusNoContent}, {role: "purchaser", want: http.StatusForbidden}} {
engine := gin.New()
engine.Use(func(c *gin.Context) {
c.Set(jwt.JwtPayloadKey, jwt.MapClaims{"rolekey": test.role})
c.Next()
})
engine.POST("/write", RequireRoleKey("admin"), func(c *gin.Context) { c.Status(http.StatusNoContent) })
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/write", nil)
engine.ServeHTTP(response, request)
if response.Code != test.want {
t.Fatalf("role %s got %d, want %d", test.role, response.Code, test.want)
}
}
}
+1 -2
View File
@@ -38,6 +38,5 @@ var CasbinExclude = []UrlInfo{
{Url: "/", Method: "GET"},
{Url: "/api/v1/server-monitor", Method: "GET"},
{Url: "/api/v1/public/uploadFile", Method: "POST"},
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
{Url: "/api/v1/sys-user", Method: "PUT"},
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
}
+5 -4
View File
@@ -210,8 +210,8 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户名称" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名称" />
<el-form-item label="登录名" prop="username">
<el-input v-model="form.username" placeholder="请输入登录时使用的账号" />
</el-form-item>
</el-col>
<el-col :span="12">
@@ -257,7 +257,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色">
<el-form-item label="角色" prop="roleId">
<el-select v-model="form.roleId" placeholder="请选择" @change="$forceUpdate()">
<el-option
v-for="item in roleOptions"
@@ -397,10 +397,11 @@ export default {
},
// 表单校验
rules: {
username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
username: [{ required: true, message: '登录名不能为空', trigger: 'blur' }],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
deptId: [{ required: true, message: '归属部门不能为空', trigger: 'blur' }],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }],
roleId: [{ required: true, message: '请选择角色', trigger: 'change' }],
email: [
{ required: true, message: '邮箱地址不能为空', trigger: 'blur' },
{ type: 'email', message: "'请输入正确的邮箱地址", trigger: ['blur', 'change'] }
@@ -3,7 +3,7 @@
<el-card class="rule-card" shadow="never">
<div class="page-heading">
<div><h1>采集规则</h1><p>通过安全表单维护 PDD 页面适配参数;规则保存后立即用于新任务。</p></div>
<el-button type="primary" :icon="Plus" @click="openCreate">创建 PDD 采集规则</el-button>
<el-button v-if="isAdmin" type="primary" :icon="Plus" @click="openCreate">创建 PDD 采集规则</el-button>
</div>
<el-form :model="query" :inline="true" class="search-form" @submit.prevent="handleQuery">
<el-form-item label="规则名称"><el-input v-model="query.name" placeholder="输入规则名称" clearable @keyup.enter="handleQuery" /></el-form-item>
@@ -15,7 +15,7 @@
<el-table-column label="类型" width="180"><template #default="{ row }"><el-tag :type="row.content.schemaVersion === 2 ? 'success' : 'info'">{{ ruleType(row.content) }}</el-tag></template></el-table-column>
<el-table-column label="规则摘要" min-width="360"><template #default="{ row }"><span class="rule-summary">{{ summary(row.content) }}</span></template></el-table-column>
<el-table-column label="更新时间" width="180"><template #default="{ row }">{{ parseTime(row.updatedAt) }}</template></el-table-column>
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openEdit(row)">编辑</el-button><el-button type="danger" link @click="remove(row)">删除</el-button></template></el-table-column>
<el-table-column v-if="isAdmin" label="操作" width="150" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openEdit(row)">编辑</el-button><el-button type="danger" link @click="remove(row)">删除</el-button></template></el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:current-page="query.page" v-model:page-size="query.pageSize" :total="total" @pagination="getList" />
</el-card>
@@ -143,6 +143,7 @@ export default {
}
},
computed: {
isAdmin() { return (this.$store.getters.roles || []).includes('admin') },
browserOptions() { const labels = { 'com.heytap.browser': 'OPPO / 一加浏览器', 'com.android.chrome': 'Google Chrome', 'com.android.browser': 'Android 系统浏览器' }; return this.constraints.browserPackages.map(value => ({ value, label: labels[value] || value })) },
directionOptions() { const labels = { up: '向上', down: '向下', left: '向左', right: '向右' }; return this.constraints.hookDirections.map(value => ({ value, label: labels[value] || value })) },
previewText() { return this.form.mode === 'v2' ? JSON.stringify(this.buildV2Content(), null, 2) : this.form.contentText }
+4 -1
View File
@@ -68,7 +68,7 @@
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="190" fixed="right">
<el-table-column v-if="isAdmin" label="操作" width="190" fixed="right">
<template #default="{ row }">
<el-button
type="danger"
@@ -116,6 +116,9 @@ export default {
query: { page: 1, pageSize: 20, name: '', status: '' }
}
},
computed: {
isAdmin() { return (this.$store.getters.roles || []).includes('admin') }
},
created() {
this.getList()
},