feat: 删除已停用的同步店铺 (#197)
This commit is contained in:
@@ -60,6 +60,19 @@ func (h *Handler) SybAllowedShopStatus(c *gin.Context) {
|
|||||||
redirectSybShops(c, message, "")
|
redirectSybShops(c, message, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SybAllowedShopDelete(c *gin.Context) {
|
||||||
|
err := service.DeleteDisabledSybAllowedShop(h.db, currentUser(c), c.PostForm("shop_id"))
|
||||||
|
if err != nil {
|
||||||
|
if service.IsValidationError(err) {
|
||||||
|
redirectSybShops(c, "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fail(c, http.StatusInternalServerError, "删除同步店铺失败,已有配置和历史货运单没有被改动。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redirectSybShops(c, "已删除停用店铺;历史货运单和同步记录保持不变", "")
|
||||||
|
}
|
||||||
|
|
||||||
func redirectSybShops(c *gin.Context, message, errorMessage string) {
|
func redirectSybShops(c *gin.Context, message, errorMessage string) {
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
if message != "" {
|
if message != "" {
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
|||||||
sybShops.GET("", h.SybAllowedShopList)
|
sybShops.GET("", h.SybAllowedShopList)
|
||||||
sybShops.POST("/create", h.SybAllowedShopCreate)
|
sybShops.POST("/create", h.SybAllowedShopCreate)
|
||||||
sybShops.POST("/status", h.SybAllowedShopStatus)
|
sybShops.POST("/status", h.SybAllowedShopStatus)
|
||||||
|
sybShops.POST("/delete", h.SybAllowedShopDelete)
|
||||||
|
|
||||||
// 4. 采集采购
|
// 4. 采集采购
|
||||||
pages.GET("/tasks", h.TaskList)
|
pages.GET("/tasks", h.TaskList)
|
||||||
|
|||||||
@@ -315,6 +315,30 @@ func TestSybPage_明确展示蝦皮商品Pdd关联来源(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSybAllowedShops_只给停用项提供二次确认删除(t *testing.T) {
|
||||||
|
templateContent, err := os.ReadFile("templates/syb/shops.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
routes, err := os.ReadFile("handler/web/web.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
page := string(templateContent)
|
||||||
|
for _, want := range []string{
|
||||||
|
`{{if not .Enabled}}`, `action="/syb/shops/delete"`,
|
||||||
|
`data-confirm-submit="确定永久删除已停用店铺“{{.ShopName}}”吗?`,
|
||||||
|
`class="danger">删除`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(page, want) {
|
||||||
|
t.Errorf("同步店铺管理页缺少删除保护 %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(routes), `sybShops.POST("/delete", h.SybAllowedShopDelete)`) {
|
||||||
|
t.Error("同步店铺删除没有挂到管理员路由组")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCatalogHistory_展示更新策略和分级统计(t *testing.T) {
|
func TestCatalogHistory_展示更新策略和分级统计(t *testing.T) {
|
||||||
list, err := os.ReadFile("templates/catalog/list.html")
|
list, err := os.ReadFile("templates/catalog/list.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -90,6 +90,20 @@ func SetSybAllowedShopEnabled(q Execer, shopID string, enabled bool, updatedAt s
|
|||||||
return affected == 1, nil
|
return affected == 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteDisabledSybAllowedShop 只删除提交瞬间仍处于停用状态的配置项。
|
||||||
|
// enabled 条件是最终并发保护,不能只依赖管理页隐藏启用项的删除按钮。
|
||||||
|
func DeleteDisabledSybAllowedShop(q Execer, shopID string) (bool, error) {
|
||||||
|
result, err := q.Exec(`DELETE FROM syb_allowed_shops WHERE shop_id=? AND enabled=0`, shopID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("删除已停用的顺运宝店铺失败: %w", err)
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("确认顺运宝店铺删除结果失败: %w", err)
|
||||||
|
}
|
||||||
|
return affected == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 会话缓存 ----------
|
// ---------- 会话缓存 ----------
|
||||||
|
|
||||||
// SaveSybSession 写入或更新顺运宝登录会话缓存(按用户名 upsert)。
|
// SaveSybSession 写入或更新顺运宝登录会话缓存(按用户名 upsert)。
|
||||||
|
|||||||
@@ -78,3 +78,22 @@ func SetSybAllowedShopEnabled(db *sql.DB, actor *model.User, shopID string, enab
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteDisabledSybAllowedShop 永久删除一条已停用配置,不影响历史货运单和同步记录。
|
||||||
|
func DeleteDisabledSybAllowedShop(db *sql.DB, actor *model.User, shopID string) error {
|
||||||
|
if actor == nil || !actor.IsAdmin() {
|
||||||
|
return ErrAdminRequired
|
||||||
|
}
|
||||||
|
shopID = strings.TrimSpace(shopID)
|
||||||
|
if shopID == "" {
|
||||||
|
return &validationError{field: "shop_id", message: "店铺编号不能为空"}
|
||||||
|
}
|
||||||
|
deleted, err := repository.DeleteDisabledSybAllowedShop(db, shopID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !deleted {
|
||||||
|
return &validationError{field: "shop_id", message: "店铺不存在或仍在启用,请刷新页面并先停用后再删除"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ func TestSybAllowedShop_管理员维护与精确去重(t *testing.T) {
|
|||||||
if err := CreateSybAllowedShop(db, admin, " qwg8fkb044 ", now); err != nil {
|
if err := CreateSybAllowedShop(db, admin, " qwg8fkb044 ", now); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if _, err := repository.UpsertSybOrder(db, model.SybOrder{
|
||||||
|
SybID: "SHOP-HISTORY", OrderNo: "ORDER-HISTORY", ShopName: "qwg8fkb044",
|
||||||
|
Quantity: 1, SybData: `{}`, CreatedAt: model.NowISO(), UpdatedAt: model.NowISO(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := repository.CreateSybSyncRun(db, model.SybSyncRun{
|
||||||
|
RunID: "SHOP-RUN-HISTORY", UserID: admin.UserID, DateFrom: "2026-08-12",
|
||||||
|
DateTo: "2026-08-12", Status: model.SybSyncRunning, StartedAt: model.NowISO(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
rows, err := ListSybAllowedShops(db, admin)
|
rows, err := ListSybAllowedShops(db, admin)
|
||||||
if err != nil || len(rows) != 1 || rows[0].ShopName != "qwg8fkb044" || !rows[0].Enabled {
|
if err != nil || len(rows) != 1 || rows[0].ShopName != "qwg8fkb044" || !rows[0].Enabled {
|
||||||
t.Fatalf("新增结果错误: rows=%+v err=%v", rows, err)
|
t.Fatalf("新增结果错误: rows=%+v err=%v", rows, err)
|
||||||
@@ -38,6 +50,25 @@ func TestSybAllowedShop_管理员维护与精确去重(t *testing.T) {
|
|||||||
if err := EnsureEnabledSybAllowedShops(db); !IsValidationError(err) {
|
if err := EnsureEnabledSybAllowedShops(db); !IsValidationError(err) {
|
||||||
t.Fatalf("空白名单应阻止同步: %v", err)
|
t.Fatalf("空白名单应阻止同步: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := DeleteDisabledSybAllowedShop(db, admin, rows[0].ShopID); err != nil {
|
||||||
|
t.Fatalf("删除已停用店铺失败: %v", err)
|
||||||
|
}
|
||||||
|
if list, err := ListSybAllowedShops(db, admin); err != nil || len(list) != 0 {
|
||||||
|
t.Fatalf("删除后仍存在: rows=%+v err=%v", list, err)
|
||||||
|
}
|
||||||
|
if count, err := repository.CountSybOrdersTotal(db); err != nil || count != 1 {
|
||||||
|
t.Fatalf("删除配置不应影响历史货运单: count=%d err=%v", count, err)
|
||||||
|
}
|
||||||
|
if count, err := repository.CountSybSyncRuns(db); err != nil || count != 1 {
|
||||||
|
t.Fatalf("删除配置不应影响同步记录: count=%d err=%v", count, err)
|
||||||
|
}
|
||||||
|
if err := CreateSybAllowedShop(db, admin, "qwg8fkb044", now.Add(2*time.Minute)); err != nil {
|
||||||
|
t.Fatalf("删除后应允许重新新增同名店铺: %v", err)
|
||||||
|
}
|
||||||
|
newRows, _ := ListSybAllowedShops(db, admin)
|
||||||
|
if err := DeleteDisabledSybAllowedShop(db, admin, newRows[0].ShopID); !IsValidationError(err) {
|
||||||
|
t.Fatalf("启用店铺必须拒绝删除: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSybAllowedShop_采购员不能管理(t *testing.T) {
|
func TestSybAllowedShop_采购员不能管理(t *testing.T) {
|
||||||
@@ -48,4 +79,7 @@ func TestSybAllowedShop_采购员不能管理(t *testing.T) {
|
|||||||
if _, err := ListSybAllowedShops(nil, purchaser); !errors.Is(err, ErrAdminRequired) {
|
if _, err := ListSybAllowedShops(nil, purchaser); !errors.Is(err, ErrAdminRequired) {
|
||||||
t.Fatalf("采购员读取管理列表应被拒绝: %v", err)
|
t.Fatalf("采购员读取管理列表应被拒绝: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := DeleteDisabledSybAllowedShop(nil, purchaser, "SHOP-1"); !errors.Is(err, ErrAdminRequired) {
|
||||||
|
t.Fatalf("采购员删除应被拒绝: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,14 @@
|
|||||||
<input type="hidden" name="enabled" value="{{if .Enabled}}0{{else}}1{{end}}">
|
<input type="hidden" name="enabled" value="{{if .Enabled}}0{{else}}1{{end}}">
|
||||||
<button type="submit">{{if .Enabled}}停用{{else}}重新启用{{end}}</button>
|
<button type="submit">{{if .Enabled}}停用{{else}}重新启用{{end}}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{{if not .Enabled}}
|
||||||
|
<form class="inline" method="post" action="/syb/shops/delete"
|
||||||
|
data-confirm-submit="确定永久删除已停用店铺“{{.ShopName}}”吗?历史货运单和同步记录不会删除;如需恢复,必须重新新增该店铺。">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||||
|
<input type="hidden" name="shop_id" value="{{.ShopID}}">
|
||||||
|
<button type="submit" class="danger">删除</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -529,8 +529,10 @@ v14 起,`stock_count` 继续表示通过完整性校验的**原始货运单数
|
|||||||
### 5.3 `syb_allowed_shops` 同步店铺准入(MySQL v14)
|
### 5.3 `syb_allowed_shops` 同步店铺准入(MySQL v14)
|
||||||
|
|
||||||
每个店铺只保留一条全局记录。`normalized_name` 是去除首尾空白后的名称并使用
|
每个店铺只保留一条全局记录。`normalized_name` 是去除首尾空白后的名称并使用
|
||||||
`utf8mb4_bin` 唯一约束;匹配不做模糊、正则或大小写折叠。停用代替硬删除,方便
|
`utf8mb4_bin` 唯一约束;匹配不做模糊、正则或大小写折叠。日常退出同步使用停用,
|
||||||
恢复和审计。同步开始时一次读取所有 `enabled=1` 的名称作为不可变快照。
|
只有已停用项允许管理员永久删除;条件删除必须再次带 `enabled=0`,避免并发重新启用
|
||||||
|
后被误删。删除配置不级联历史货运单或同步记录。同步开始时一次读取所有
|
||||||
|
`enabled=1` 的名称作为不可变快照。
|
||||||
|
|
||||||
`[必须]` 没有任何启用店铺时同步关闭失败,不请求顺运宝、不推进游标。该表只控制
|
`[必须]` 没有任何启用店铺时同步关闭失败,不请求顺运宝、不推进游标。该表只控制
|
||||||
后续同步是否入库,不自动删除历史 `syb_orders`。
|
后续同步是否入库,不自动删除历史 `syb_orders`。
|
||||||
|
|||||||
@@ -473,7 +473,9 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
|||||||
日期始终显示在主工具条,按 UTC+8 解释且两端都包含。首次打开页面固定默认昨天
|
日期始终显示在主工具条,按 UTC+8 解释且两端都包含。首次打开页面固定默认昨天
|
||||||
到今天,不因覆盖游标位置自动扩大范围;需要补历史缺口时由采购员明确选择日期。
|
到今天,不因覆盖游标位置自动扩大范围;需要补历史缺口时由采购员明确选择日期。
|
||||||
所有账号都能看到当前启用同步店铺数量;只有管理员显示“同步店铺”管理入口。
|
所有账号都能看到当前启用同步店铺数量;只有管理员显示“同步店铺”管理入口。
|
||||||
管理页使用可见的“店铺名称”标签,支持新增、停用和重新启用,不提供硬删除。
|
管理页使用可见的“店铺名称”标签,支持新增、停用、重新启用,以及永久删除已停用
|
||||||
|
店铺。启用店铺不显示删除入口,服务端仍必须按停用状态条件删除;确认文案显示店铺
|
||||||
|
名称,并说明历史货运单和同步记录不会删除。
|
||||||
空状态必须明确提示先新增并启用至少一个店铺,否则同步会安全停止。
|
空状态必须明确提示先新增并启用至少一个店铺,否则同步会安全停止。
|
||||||
|
|
||||||
`[必须]` 「同步」是唯一同步入口:
|
`[必须]` 「同步」是唯一同步入口:
|
||||||
|
|||||||
Reference in New Issue
Block a user