chore(服务端): 初始化工程仓库
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Color struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Hex string `json:"hex"`
|
||||
Location string `json:"location"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
type Pattern struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Category string `json:"category"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Cells []*string `json:"cells"`
|
||||
Colors []Color `json:"colors"`
|
||||
BeadCount int `json:"beadCount"`
|
||||
OwnerID string `json:"ownerId,omitempty"`
|
||||
Visibility string `json:"visibility"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
StoreID string `json:"storeId"`
|
||||
Demo bool `json:"demo"`
|
||||
}
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
StoreID string `json:"storeId"`
|
||||
}
|
||||
type Store struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Hours string `json:"hours"`
|
||||
Notice string `json:"notice"`
|
||||
LocalBaseURL string `json:"localBaseUrl"`
|
||||
PublicBaseURL string `json:"publicBaseUrl"`
|
||||
}
|
||||
type Table struct {
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
type Visit struct {
|
||||
ID string `json:"id"`
|
||||
StoreID string `json:"storeId"`
|
||||
TableCode string `json:"tableCode"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
type Request struct {
|
||||
ID string `json:"id"`
|
||||
StoreID string `json:"storeId"`
|
||||
UserID string `json:"userId"`
|
||||
TableCode string `json:"tableCode"`
|
||||
Kind string `json:"kind"`
|
||||
Note string `json:"note"`
|
||||
Status string `json:"status"`
|
||||
PatternID string `json:"patternId,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
var hexColor = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
||||
|
||||
func ValidateColors(cs []Color) error {
|
||||
seen := map[string]bool{}
|
||||
if len(cs) == 0 || len(cs) > 512 {
|
||||
return fmt.Errorf("色板需包含 1–512 色")
|
||||
}
|
||||
for _, c := range cs {
|
||||
if strings.TrimSpace(c.Code) == "" || len(c.Code) > 32 || seen[c.Code] || !hexColor.MatchString(c.Hex) {
|
||||
return fmt.Errorf("色号重复或颜色格式错误")
|
||||
}
|
||||
seen[c.Code] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (p *Pattern) Validate() error {
|
||||
if strings.TrimSpace(p.Title) == "" || len(p.Title) > 200 || p.Width < 1 || p.Height < 1 || p.Width > 200 || p.Height > 200 || len(p.Cells) != p.Width*p.Height {
|
||||
return fmt.Errorf("图纸名称或尺寸无效(最大 200×200)")
|
||||
}
|
||||
if p.Visibility != "public" && p.Visibility != "private" {
|
||||
return fmt.Errorf("可见性无效")
|
||||
}
|
||||
if err := ValidateColors(p.Colors); err != nil {
|
||||
return err
|
||||
}
|
||||
cs := map[string]bool{}
|
||||
for _, c := range p.Colors {
|
||||
cs[c.Code] = true
|
||||
}
|
||||
p.BeadCount = 0
|
||||
for _, c := range p.Cells {
|
||||
if c != nil {
|
||||
if !cs[*c] {
|
||||
return fmt.Errorf("网格含未知色号")
|
||||
}
|
||||
p.BeadCount++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func Transition(role, old, next string, own bool) bool {
|
||||
if role == "admin" || role == "staff" {
|
||||
return old == "pending" && next == "accepted" || old == "accepted" && next == "completed"
|
||||
}
|
||||
return own && old == "pending" && next == "cancelled"
|
||||
}
|
||||
func Fingerprint(kind, note, pattern string) string {
|
||||
b, _ := json.Marshal([]string{kind, note, pattern})
|
||||
h := sha256.Sum256(b)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
func CanRead(p Pattern, u User) bool {
|
||||
return p.Visibility == "public" || p.OwnerID == u.ID && u.ID != "" || (u.Role == "admin" || u.Role == "staff") && p.StoreID == u.StoreID
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPatternValidation(t *testing.T) {
|
||||
p := Pattern{Title: "demo", Width: 2, Height: 1, Cells: []*string{str("R"), nil}, Colors: []Color{{Code: "R", Hex: "#ff0000"}}, Visibility: "private"}
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.Cells[0] = str("missing")
|
||||
if p.Validate() == nil {
|
||||
t.Fatal("unknown color accepted")
|
||||
}
|
||||
p.Cells = []*string{}
|
||||
if p.Validate() == nil {
|
||||
t.Fatal("wrong grid size accepted")
|
||||
}
|
||||
}
|
||||
func str(s string) *string { return &s }
|
||||
func TestTransitionAuthorization(t *testing.T) {
|
||||
cases := []struct {
|
||||
role, old, next string
|
||||
own bool
|
||||
ok bool
|
||||
}{{"guest", "pending", "cancelled", true, true}, {"guest", "accepted", "cancelled", true, false}, {"guest", "pending", "cancelled", false, false}, {"staff", "pending", "completed", false, false}, {"staff", "pending", "accepted", false, true}, {"admin", "accepted", "completed", false, true}}
|
||||
for _, c := range cases {
|
||||
if Transition(c.role, c.old, c.next, c.own) != c.ok {
|
||||
t.Fatalf("%+v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestIdempotencyContent(t *testing.T) {
|
||||
if Fingerprint("help", "one", "") == Fingerprint("help", "two", "") {
|
||||
t.Fatal("different contents collide")
|
||||
}
|
||||
if Fingerprint("help", "one", "") != Fingerprint("help", "one", "") {
|
||||
t.Fatal("unstable fingerprint")
|
||||
}
|
||||
}
|
||||
func TestPrivateIsolation(t *testing.T) {
|
||||
p := Pattern{OwnerID: "owner", Visibility: "private", StoreID: "s1"}
|
||||
if CanRead(p, User{ID: "stranger", Role: "guest", StoreID: "s1"}) {
|
||||
t.Fatal("private pattern leaked")
|
||||
}
|
||||
if CanRead(p, User{ID: "admin", Role: "admin", StoreID: "s2"}) {
|
||||
t.Fatal("cross-store admin read")
|
||||
}
|
||||
if !CanRead(p, User{ID: "owner"}) {
|
||||
t.Fatal("owner denied")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user