chore(服务端): 初始化工程仓库

This commit is contained in:
leon_mac
2026-10-18 15:11:44 +08:00
commit 13c8bcbf3c
23 changed files with 2543 additions and 0 deletions
+126
View File
@@ -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
}
+51
View File
@@ -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")
}
}