20260706213434

This commit is contained in:
oneao committed 2026-07-06 21:34:34 +08:00
1 parent 4d1e6c8b8f
commit f8e16efa83
23 files changed
+1232 -765

No files matched your search

-54
View File
@@ -1,54 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"allapp-go/internal/bootstrap/boots"
"allapp-go/internal/config"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: go run ./cmd/apply_sql <sql-file>")
os.Exit(1)
}
sqlPath, err := filepath.Abs(os.Args[1])
if err != nil {
exitErr("resolve sql path", err)
}
sqlBytes, err := os.ReadFile(sqlPath)
if err != nil {
exitErr("read sql file", err)
}
cfg, err := config.Load()
if err != nil {
exitErr("load config", err)
}
pool, err := boots.InitPgsql(cfg)
if err != nil {
exitErr("connect postgres", err)
}
defer pool.Close()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if _, err := pool.Exec(ctx, string(sqlBytes)); err != nil {
exitErr("execute sql", err)
}
fmt.Printf("applied sql: %s\n", sqlPath)
}
func exitErr(step string, err error) {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
os.Exit(1)
}
@@ -1,59 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"time"
"allapp-go/internal/bootstrap/boots"
"allapp-go/internal/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
exitErr("load config", err)
}
pool, err := boots.InitPgsql(cfg)
if err != nil {
exitErr("connect postgres", err)
}
defer pool.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rows, err := pool.Query(ctx, `
select table_name
from information_schema.tables
where table_schema = 'public'
and table_name like 'b_clothing_%'
order by table_name
`)
if err != nil {
exitErr("query tables", err)
}
defer rows.Close()
count := 0
for rows.Next() {
var tableName string
if err := rows.Scan(&tableName); err != nil {
exitErr("scan table", err)
}
count++
fmt.Println(tableName)
}
if err := rows.Err(); err != nil {
exitErr("read rows", err)
}
fmt.Printf("total=%d\n", count)
}
func exitErr(step string, err error) {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
os.Exit(1)
}
@@ -1,66 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"time"
"allapp-go/internal/bootstrap/boots"
"allapp-go/internal/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
exitErr("load config", err)
}
pool, err := boots.InitPgsql(cfg)
if err != nil {
exitErr("connect postgres", err)
}
defer pool.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rows, err := pool.Query(ctx, `
select table_name, column_name
from information_schema.columns
where table_schema = 'public'
and table_name in (
'b_finance_category_default',
'b_finance_category',
'b_clothing_clothes',
'b_clothing_outfit'
)
and column_name in ('icon_mime', 'image_mime', 'cover_mime')
order by table_name, column_name
`)
if err != nil {
exitErr("query columns", err)
}
defer rows.Close()
count := 0
for rows.Next() {
var tableName string
var columnName string
if err := rows.Scan(&tableName, &columnName); err != nil {
exitErr("scan column", err)
}
count++
fmt.Printf("%s.%s\n", tableName, columnName)
}
if err := rows.Err(); err != nil {
exitErr("read rows", err)
}
fmt.Printf("removed_mime_columns_remaining=%d\n", count)
}
func exitErr(step string, err error) {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
os.Exit(1)
}
@@ -1,233 +0,0 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"allapp-go/internal/bootstrap/boots"
"allapp-go/internal/config"
"allapp-go/pkg/s3store"
"allapp-go/pkg/uniqueid"
"github.com/jackc/pgx/v5"
)
type iconManifestItem struct {
Name string `json:"name"`
Type int16 `json:"type"`
TypeName string `json:"typeName"`
Source string `json:"source"`
File string `json:"file"`
Key string `json:"key"`
Mime string `json:"mime"`
SortNumber int `json:"sortNumber"`
Width int `json:"width"`
Height int `json:"height"`
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
cfg, err := config.Load()
if err != nil {
exitErr("load config", err)
}
boots.InitUniqueId(cfg)
s3store.NewS3Client(cfg)
pool, err := boots.InitPgsql(cfg)
if err != nil {
exitErr("connect postgres", err)
}
defer pool.Close()
workspaceRoot, err := filepath.Abs("..")
if err != nil {
exitErr("resolve workspace", err)
}
manifestPath := filepath.Join(workspaceRoot, "tools", "finance-category-icons", "dist", "manifest.json")
manifestBytes, err := os.ReadFile(manifestPath)
if err != nil {
exitErr("read manifest", err)
}
var items []iconManifestItem
if err := json.Unmarshal(manifestBytes, &items); err != nil {
exitErr("parse manifest", err)
}
if len(items) == 0 {
exitErr("validate manifest", fmt.Errorf("manifest is empty"))
}
tableName, err := resolveDefaultCategoryTable(ctx, pool)
if err != nil {
exitErr("resolve table", err)
}
tx, err := pool.Begin(ctx)
if err != nil {
exitErr("begin transaction", err)
}
defer func() {
_ = tx.Rollback(ctx)
}()
uploaded := 0
inserted := 0
updated := 0
for _, item := range items {
if err := validateItem(item); err != nil {
exitErr("validate item", err)
}
filePath := filepath.Join(workspaceRoot, filepath.FromSlash(item.File))
data, err := os.ReadFile(filePath)
if err != nil {
exitErr("read converted icon", err)
}
if err := s3store.UploadToRustFS(ctx, bytes.NewReader(data), item.Key, int64(len(data)), item.Mime); err != nil {
exitErr("upload "+item.Key, err)
}
uploaded++
existingID, found, err := findExistingCategory(ctx, tx, tableName, item.Name, item.Type)
if err != nil {
exitErr("query existing category", err)
}
now := time.Now()
if found {
if err := updateCategory(ctx, tx, tableName, existingID, item, now); err != nil {
exitErr("update category", err)
}
updated++
continue
}
if err := insertCategory(ctx, tx, tableName, uniqueid.NextId(), item, now); err != nil {
exitErr("insert category", err)
}
inserted++
}
if err := tx.Commit(ctx); err != nil {
exitErr("commit transaction", err)
}
expenseCount, incomeCount, err := countImportedCategories(ctx, pool, tableName)
if err != nil {
exitErr("verify imported categories", err)
}
fmt.Printf("Imported finance category icons successfully.\n")
fmt.Printf("table=%s uploaded=%d inserted=%d updated=%d total=%d\n", tableName, uploaded, inserted, updated, len(items))
fmt.Printf("verified prefix=finance-category-default/ expense=%d income=%d total=%d\n", expenseCount, incomeCount, expenseCount+incomeCount)
for _, item := range items {
fmt.Printf("%s type=%d sort=%d key=%s\n", item.Name, item.Type, item.SortNumber, item.Key)
}
}
func validateItem(item iconManifestItem) error {
if item.Name == "" || item.Key == "" || item.File == "" || item.Mime != "image/webp" {
return fmt.Errorf("invalid manifest item: %+v", item)
}
if item.Type != 0 && item.Type != 1 {
return fmt.Errorf("invalid type for %s: %d", item.Name, item.Type)
}
if item.Width != 256 || item.Height != 256 {
return fmt.Errorf("invalid dimensions for %s: %dx%d", item.Name, item.Width, item.Height)
}
return nil
}
func resolveDefaultCategoryTable(ctx context.Context, q pgxQueryer) (string, error) {
candidates := []string{"finance_category_default", "b_finance_category_default"}
for _, table := range candidates {
var exists bool
if err := q.QueryRow(ctx, "select to_regclass($1) is not null", table).Scan(&exists); err != nil {
return "", err
}
if exists {
return table, nil
}
}
return "", fmt.Errorf("neither finance_category_default nor b_finance_category_default exists")
}
func findExistingCategory(ctx context.Context, tx pgx.Tx, tableName string, name string, categoryType int16) (int64, bool, error) {
query := fmt.Sprintf("select id from %s where name=$1 and type=$2 order by id limit 1", safeTableName(tableName))
var id int64
err := tx.QueryRow(ctx, query, name, categoryType).Scan(&id)
if err == nil {
return id, true, nil
}
if err == pgx.ErrNoRows {
return 0, false, nil
}
return 0, false, err
}
func updateCategory(ctx context.Context, tx pgx.Tx, tableName string, id int64, item iconManifestItem, now time.Time) error {
query := fmt.Sprintf(`
update %s
set icon_file_key=$1, sort_number=$2, status=1, update_time=$3
where id=$4
`, safeTableName(tableName))
_, err := tx.Exec(ctx, query, item.Key, item.SortNumber, now, id)
return err
}
func insertCategory(ctx context.Context, tx pgx.Tx, tableName string, id int64, item iconManifestItem, now time.Time) error {
query := fmt.Sprintf(`
insert into %s (id, name, type, icon_file_key, sort_number, status, create_time, update_time)
values ($1, $2, $3, $4, $5, 1, $6, $6)
`, safeTableName(tableName))
_, err := tx.Exec(ctx, query, id, item.Name, item.Type, item.Key, item.SortNumber, now)
return err
}
func countImportedCategories(ctx context.Context, q pgxQueryer, tableName string) (int, int, error) {
query := fmt.Sprintf(`
select
count(*) filter (where type = 0),
count(*) filter (where type = 1)
from %s
where icon_file_key like 'finance-category-default/%%'
and status = 1
`, safeTableName(tableName))
var expenseCount int
var incomeCount int
if err := q.QueryRow(ctx, query).Scan(&expenseCount, &incomeCount); err != nil {
return 0, 0, err
}
return expenseCount, incomeCount, nil
}
func safeTableName(tableName string) string {
switch tableName {
case "finance_category_default", "b_finance_category_default":
return tableName
default:
panic("unsafe table name: " + tableName)
}
}
type pgxQueryer interface {
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
func exitErr(step string, err error) {
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
os.Exit(1)
}
+27
View File
@@ -2,6 +2,7 @@
-- 餐食模块 数据库表结构
-- ============================================================
drop table if exists b_meal_plan_rating;
drop table if exists b_meal_dish_tag;
drop table if exists b_meal_dish_ingredient;
drop table if exists b_meal_dish_step;
@@ -187,6 +188,24 @@ create table b_meal_plan_log (
update_by bigint
);
-- ============================================================
-- 餐食安排评分
-- rating: 1-5 星
-- 同一家庭、同一安排、同一评分人只有一条记录(通过索引保证唯一)
-- ============================================================
create table b_meal_plan_rating (
id bigint primary key,
family_id bigint not null,
plan_id bigint not null,
rater_id bigint not null,
rating smallint not null,
content varchar(256) default '',
create_time timestamptz,
update_time timestamptz,
create_by bigint,
update_by bigint
);
-- ============================================================
-- 索引
-- ============================================================
@@ -245,3 +264,11 @@ create index idx_b_meal_plan_log_plan
-- 活动日志:按安排和类型筛选
create index idx_b_meal_plan_log_plan_type
on b_meal_plan_log (family_id, plan_id, type);
-- 评分:按安排查询
create index idx_b_meal_plan_rating_plan
on b_meal_plan_rating (family_id, plan_id, id);
-- 评分:同一家庭、同一安排、同一评分人唯一
create unique index idx_b_meal_plan_rating_plan_rater
on b_meal_plan_rating (family_id, plan_id, rater_id);