From 1aecb4424c7adb7f69c1e819b8640b3cc0e1e146 Mon Sep 17 00:00:00 2001 From: xkm Date: Tue, 7 Apr 2026 21:21:18 +0800 Subject: [PATCH] feat: support basic user system --- cmd/api/main.go | 11 +- db/migration/000002_v2.down.sql | 53 +++++++++ db/migration/000002_v2.up.sql | 77 +++++++++++++ db/migration/000003_add_jwt_config.down.sql | 1 + db/migration/000003_add_jwt_config.up.sql | 11 ++ db/query/config.sql | 4 + db/query/market.sql | 34 +++--- db/query/user.sql | 4 + go.mod | 15 ++- go.sum | 30 ++++++ internal/config/config.go | 22 ++++ internal/handler/auth.go | 73 +++++++++++++ internal/repository/config.sql.go | 28 +++++ internal/repository/market.sql.go | 114 ++++++++++++++++---- internal/repository/models.go | 45 ++++++++ internal/repository/user.sql.go | 32 ++++++ internal/router/router.go | 13 ++- internal/service/auth.go | 44 ++++++++ internal/service/auth_test.go | 13 +++ internal/service/jwt_utils.go | 1 + internal/service/service.go | 65 ++++++----- internal/service/types.go | 29 +++++ 22 files changed, 654 insertions(+), 65 deletions(-) create mode 100644 db/migration/000002_v2.down.sql create mode 100644 db/migration/000002_v2.up.sql create mode 100644 db/migration/000003_add_jwt_config.down.sql create mode 100644 db/migration/000003_add_jwt_config.up.sql create mode 100644 db/query/config.sql create mode 100644 db/query/user.sql create mode 100644 internal/config/config.go create mode 100644 internal/handler/auth.go create mode 100644 internal/repository/config.sql.go create mode 100644 internal/repository/user.sql.go create mode 100644 internal/service/auth.go create mode 100644 internal/service/auth_test.go create mode 100644 internal/service/jwt_utils.go diff --git a/cmd/api/main.go b/cmd/api/main.go index bd6e3b6..b034c0f 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -4,9 +4,11 @@ import ( "context" "database/sql" "log" + "log/slog" "net/http" "os" + "gitea.starryskymeow.cn/B309/datamarket/internal/config" "gitea.starryskymeow.cn/B309/datamarket/internal/repository" "gitea.starryskymeow.cn/B309/datamarket/internal/router" "gitea.starryskymeow.cn/B309/datamarket/internal/service" @@ -59,8 +61,13 @@ func main() { } queries := repository.New(pool) - appService := service.New(queries) - r := router.New(appService) + cfg, err := config.New(queries) + if err != nil { + slog.Error("Error loading config") + log.Fatal(err) + } + appService := service.New(queries, cfg) + r := router.New(appService, cfg) addr := os.Getenv("SERVER_ADDR") diff --git a/db/migration/000002_v2.down.sql b/db/migration/000002_v2.down.sql new file mode 100644 index 0000000..1953cec --- /dev/null +++ b/db/migration/000002_v2.down.sql @@ -0,0 +1,53 @@ +-- remove user reference from orders +ALTER TABLE orders + DROP COLUMN IF EXISTS user_id; + +-- remove agent columns from validations +ALTER TABLE validations + DROP COLUMN IF EXISTS agent_risk_notice; + +ALTER TABLE validations + DROP COLUMN IF EXISTS agent_delivery_advice; + +ALTER TABLE validations + DROP COLUMN IF EXISTS agent_continue_trade_advice; + +ALTER TABLE validations + DROP COLUMN IF EXISTS agent_validation_explanation; + +-- remove agent columns from pricing_results +ALTER TABLE pricing_results + DROP COLUMN IF EXISTS agent_next_action; + +ALTER TABLE pricing_results + DROP COLUMN IF EXISTS agent_budget_advice; + +ALTER TABLE pricing_results + DROP COLUMN IF EXISTS agent_risk_advice; + +ALTER TABLE pricing_results + DROP COLUMN IF EXISTS agent_task_match_explanation; + +-- remove user reference from buyer_requests +ALTER TABLE buyer_requests + DROP COLUMN IF EXISTS user_id; + +-- remove agent columns from data_assets +ALTER TABLE data_assets + DROP COLUMN IF EXISTS agent_asset_explanation; + +ALTER TABLE data_assets + DROP COLUMN IF EXISTS agent_risk_per_mission_advice; + +ALTER TABLE data_assets + DROP COLUMN IF EXISTS agent_recommended_tasks; + +ALTER TABLE data_assets + DROP COLUMN IF EXISTS agent_asset_summary; + +-- remove user reference from data_assets +ALTER TABLE data_assets + DROP COLUMN IF EXISTS user_id; + +-- drop users table +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/db/migration/000002_v2.up.sql b/db/migration/000002_v2.up.sql new file mode 100644 index 0000000..bf2185a --- /dev/null +++ b/db/migration/000002_v2.up.sql @@ -0,0 +1,77 @@ +-- new user table +CREATE TABLE IF NOT EXISTS users +( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + username text NOT NULL UNIQUE, + password text NOT NULL, + role text NOT NULL, -- admin user + display_name text NOT NULL, + account_status text NOT NULL DEFAULT 'active', + last_login_at timestamptz, + created_at timestamptz DEFAULT now() +); + +-- add user reference to data_assets +ALTER TABLE data_assets + ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES users (id); + +-- add agent columns to data_assets +ALTER TABLE data_assets + ADD COLUMN IF NOT EXISTS agent_asset_summary text; +COMMENT ON COLUMN data_assets.agent_asset_summary IS '智能助手对当前资产的简短画像总结'; + +ALTER TABLE data_assets + ADD COLUMN IF NOT EXISTS agent_recommended_tasks jsonb; +COMMENT ON COLUMN data_assets.agent_recommended_tasks IS '推荐的适用任务列表'; + +ALTER TABLE data_assets + ADD COLUMN IF NOT EXISTS agent_risk_per_mission_advice text; +COMMENT ON COLUMN data_assets.agent_risk_per_mission_advice IS '当前资产更适合的权限与风险建议'; + +ALTER TABLE data_assets + ADD COLUMN IF NOT EXISTS agent_asset_explanation text; +COMMENT ON COLUMN data_assets.agent_asset_explanation IS '为什么该资产当前基础价值较高/中/低'; + +-- add user reference to buyer_requests +ALTER TABLE buyer_requests + ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES users (id); + +-- add agent to pricing_results +ALTER TABLE pricing_results + ADD COLUMN IF NOT EXISTS agent_task_match_explanation text; +COMMENT ON COLUMN pricing_results.agent_task_match_explanation IS '解释当前数据与任务的匹配关系'; + +ALTER TABLE pricing_results + ADD COLUMN IF NOT EXISTS agent_risk_advice text; +COMMENT ON COLUMN pricing_results.agent_risk_advice IS '解释当前隐私风险与权限建议'; + +ALTER TABLE pricing_results + ADD COLUMN IF NOT EXISTS agent_budget_advice text; +COMMENT ON COLUMN pricing_results.agent_budget_advice IS '根据预算给出的建议'; + +ALTER TABLE pricing_results + ADD COLUMN IF NOT EXISTS agent_next_action text; +COMMENT ON COLUMN pricing_results.agent_next_action IS '建议接下来做什么'; + +-- add agent to +ALTER TABLE validations + ADD COLUMN IF NOT EXISTS agent_validation_explanation text; +COMMENT ON COLUMN validations.agent_validation_explanation IS '对当前验证结果的自然语言解释'; + +ALTER TABLE validations + ADD COLUMN IF NOT EXISTS agent_continue_trade_advice text; +COMMENT ON COLUMN validations.agent_continue_trade_advice IS '对是否继续成交的建议'; + +ALTER TABLE validations + ADD COLUMN IF NOT EXISTS agent_delivery_advice text; +COMMENT ON COLUMN validations.agent_delivery_advice IS '对交付方式的建议'; + +ALTER TABLE validations + ADD COLUMN IF NOT EXISTS agent_risk_notice text; +COMMENT ON COLUMN validations.agent_risk_notice IS '对后续交易的风险提示'; + +-- add user to order +ALTER TABLE orders + ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES users (id); + + diff --git a/db/migration/000003_add_jwt_config.down.sql b/db/migration/000003_add_jwt_config.down.sql new file mode 100644 index 0000000..a242012 --- /dev/null +++ b/db/migration/000003_add_jwt_config.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS config; \ No newline at end of file diff --git a/db/migration/000003_add_jwt_config.up.sql b/db/migration/000003_add_jwt_config.up.sql new file mode 100644 index 0000000..958e35e --- /dev/null +++ b/db/migration/000003_add_jwt_config.up.sql @@ -0,0 +1,11 @@ +-- add config(jwt) +CREATE TABLE IF NOT EXISTS config +( + id integer PRIMARY KEY, -- always 0 + "Jwt.Alg" text NOT NULL DEFAULT 'HS256', + "Jwt.SignKey" text NOT NULL DEFAULT gen_random_uuid(), + "Jwt.VerifyKye" text DEFAULT NULL +); + +INSERT INTO config (id) +VALUES (0); \ No newline at end of file diff --git a/db/query/config.sql b/db/query/config.sql new file mode 100644 index 0000000..141dc66 --- /dev/null +++ b/db/query/config.sql @@ -0,0 +1,4 @@ +-- name: GetConfig :one +SELECT * +FROM config +WHERE id = 0; \ No newline at end of file diff --git a/db/query/market.sql b/db/query/market.sql index e45537c..7efb9f1 100644 --- a/db/query/market.sql +++ b/db/query/market.sql @@ -53,12 +53,12 @@ WHERE ( ); -- name: GetDataAsset :one -SELECT id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at +SELECT * FROM data_assets WHERE id = $1; -- name: ListDataAssets :many -SELECT id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at +SELECT * FROM data_assets WHERE ( NULLIF(sqlc.narg(keyword)::text, '') IS NULL @@ -89,7 +89,7 @@ UPDATE data_assets SET asset_status = $2, updated_at = now() WHERE id = $1 -RETURNING id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at; +RETURNING *; -- name: CreateBuyerRequest :one INSERT INTO buyer_requests ( @@ -104,15 +104,15 @@ INSERT INTO buyer_requests ( request_status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) -RETURNING id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at; +RETURNING *; -- name: GetBuyerRequest :one -SELECT id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at +SELECT * FROM buyer_requests WHERE id = $1; -- name: ListBuyerRequests :many -SELECT id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at +SELECT * FROM buyer_requests ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2; @@ -133,15 +133,15 @@ INSERT INTO pricing_results ( pricing_status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) -RETURNING id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at; +RETURNING *; -- name: GetPricingResult :one -SELECT id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at +SELECT * FROM pricing_results WHERE id = $1; -- name: ListPricingResults :many -SELECT id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at +SELECT * FROM pricing_results ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2; @@ -160,7 +160,7 @@ INSERT INTO validations ( validation_finished_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at; +RETURNING *; -- name: UpdateValidationResult :one UPDATE validations @@ -171,15 +171,15 @@ SET validation_status = $2, continue_recommendation = $6, validation_finished_at = $7 WHERE id = $1 -RETURNING id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at; +RETURNING *; -- name: GetValidation :one -SELECT id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at +SELECT * FROM validations WHERE id = $1; -- name: ListValidations :many -SELECT id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at +SELECT * FROM validations ORDER BY validation_created_at DESC, id DESC LIMIT $1 OFFSET $2; @@ -203,15 +203,15 @@ INSERT INTO orders ( order_status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) -RETURNING id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at; +RETURNING *; -- name: GetOrder :one -SELECT id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at +SELECT * FROM orders WHERE id = $1; -- name: ListOrders :many -SELECT id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at +SELECT * FROM orders WHERE NULLIF(sqlc.narg(order_status)::text, '') IS NULL OR order_status = sqlc.narg(order_status)::text @@ -229,4 +229,4 @@ UPDATE orders SET order_status = $2, order_updated_at = now() WHERE id = $1 -RETURNING id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at; +RETURNING *; diff --git a/db/query/user.sql b/db/query/user.sql new file mode 100644 index 0000000..bdb0b92 --- /dev/null +++ b/db/query/user.sql @@ -0,0 +1,4 @@ +-- name: GetUserByUsername :one +SELECT * +FROM users +WHERE username = $1; \ No newline at end of file diff --git a/go.mod b/go.mod index 1ebe80d..23f66e7 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,21 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/go-chi/jwtauth/v5 v5.4.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.3 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/text v0.31.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index fcc82fa..51af0f6 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmC github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -25,12 +27,16 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTwYI= +github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= @@ -45,6 +51,16 @@ github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= +github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE= +github.com/lestrrat-go/jwx/v3 v3.0.2 h1:N+XLjTJEzDZRP3S0SezclXFAfopwL+o5vaL+qg6rX1I= +github.com/lestrrat-go/jwx/v3 v3.0.2/go.mod h1:qO9w1qkQH77a0r9OXNM33YQPnV/evetKYRg58h1rBNE= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -62,9 +78,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -77,12 +97,22 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d4b96a2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "context" + + "gitea.starryskymeow.cn/B309/datamarket/internal/repository" + "github.com/go-chi/jwtauth/v5" +) + +type Config struct { + JWTAuth *jwtauth.JWTAuth +} + +func New(repo *repository.Queries) (*Config, error) { + config := new(Config) + cfg, err := repo.GetConfig(context.Background()) + if err != nil { + return nil, err + } + config.JWTAuth = jwtauth.New(cfg.JwtAlg, []byte(cfg.JwtSignKey), nil) + return config, nil +} diff --git a/internal/handler/auth.go b/internal/handler/auth.go new file mode 100644 index 0000000..2450444 --- /dev/null +++ b/internal/handler/auth.go @@ -0,0 +1,73 @@ +package handler + +import ( + "fmt" + "net/http" + "regexp" + + "gitea.starryskymeow.cn/B309/datamarket/internal/service" + "github.com/go-chi/jwtauth/v5" + "github.com/go-chi/render" +) + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +const usernameExpr = `^\S{3,32}$` +const passwordExpr = `^\S{8,64}$` + +func (req *LoginRequest) Bind(_ *http.Request) error { + // username + if ok, _ := regexp.MatchString(usernameExpr, req.Username); !ok { + return fmt.Errorf("invalid username, must match %q", usernameExpr) + } + + // password + if ok, _ := regexp.MatchString(passwordExpr, req.Password); !ok { + return fmt.Errorf("invalid password, must match %q", passwordExpr) + } + + return nil +} + +// LoginHandler POST /api/auth/login +func LoginHandler(auth service.AuthService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + req := &LoginRequest{} + if err := render.Bind(r, req); err != nil { + w.WriteHeader(http.StatusBadRequest) + renderServiceError(w, r, err) + return + } + user, err := auth.VerifyUser(r.Context(), service.VerifyUserInput{ + Username: req.Username, + Password: req.Password, + }) + if err != nil { + renderServiceError(w, r, err) + return + } + http.SetCookie(w, &http.Cookie{ + Name: "jwt", + Value: user.Token, + Path: "/", + HttpOnly: true, + Secure: true, + MaxAge: 60 * 60 * 24, // 1d + SameSite: http.SameSiteLaxMode, + }) + renderSuccess(w, r, http.StatusAccepted, "登录成功", user) + } +} + +func MeHandler(auth service.AuthService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, claims, err := jwtauth.FromContext(r.Context()) + if err != nil { + renderServiceError(w, r, err) + } + renderSuccess(w, r, http.StatusOK, "logged in", claims) + } +} diff --git a/internal/repository/config.sql.go b/internal/repository/config.sql.go new file mode 100644 index 0000000..2b00639 --- /dev/null +++ b/internal/repository/config.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: config.sql + +package repository + +import ( + "context" +) + +const getConfig = `-- name: GetConfig :one +SELECT id, "Jwt.Alg", "Jwt.SignKey", "Jwt.VerifyKye" +FROM config +WHERE id = 0 +` + +func (q *Queries) GetConfig(ctx context.Context) (Config, error) { + row := q.db.QueryRow(ctx, getConfig) + var i Config + err := row.Scan( + &i.ID, + &i.JwtAlg, + &i.JwtSignKey, + &i.JwtVerifyKye, + ) + return i, err +} diff --git a/internal/repository/market.sql.go b/internal/repository/market.sql.go index 6db2aa7..c15a10f 100644 --- a/internal/repository/market.sql.go +++ b/internal/repository/market.sql.go @@ -97,7 +97,7 @@ INSERT INTO buyer_requests ( request_status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) -RETURNING id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at +RETURNING id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at, user_id ` type CreateBuyerRequestParams struct { @@ -138,6 +138,7 @@ func (q *Queries) CreateBuyerRequest(ctx context.Context, arg CreateBuyerRequest &i.RequestStatus, &i.CreatedAt, &i.UpdatedAt, + &i.UserID, ) return i, err } @@ -195,7 +196,33 @@ type CreateDataAssetParams struct { AssetStatus string `json:"asset_status"` } -func (q *Queries) CreateDataAsset(ctx context.Context, arg CreateDataAssetParams) (DataAsset, error) { +type CreateDataAssetRow struct { + ID pgtype.UUID `json:"id"` + AssetName string `json:"asset_name"` + AssetType string `json:"asset_type"` + Domain string `json:"domain"` + ApplicationScene pgtype.Text `json:"application_scene"` + DataDescription string `json:"data_description"` + DataScale string `json:"data_scale"` + CollectionMethod string `json:"collection_method"` + LabelingStatus pgtype.Text `json:"labeling_status"` + UpdateFrequency pgtype.Text `json:"update_frequency"` + PrivacyLevel string `json:"privacy_level"` + PermissionMode string `json:"permission_mode"` + SupportsValidation bool `json:"supports_validation"` + SellerExpectedPriceMin pgtype.Numeric `json:"seller_expected_price_min"` + SellerExpectedPriceMax pgtype.Numeric `json:"seller_expected_price_max"` + QualityLevel pgtype.Text `json:"quality_level"` + ScarcityLevel pgtype.Text `json:"scarcity_level"` + BaseValueScore pgtype.Numeric `json:"base_value_score"` + BasePriceMin pgtype.Numeric `json:"base_price_min"` + BasePriceMax pgtype.Numeric `json:"base_price_max"` + AssetStatus string `json:"asset_status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +func (q *Queries) CreateDataAsset(ctx context.Context, arg CreateDataAssetParams) (CreateDataAssetRow, error) { row := q.db.QueryRow(ctx, createDataAsset, arg.AssetName, arg.AssetType, @@ -218,7 +245,7 @@ func (q *Queries) CreateDataAsset(ctx context.Context, arg CreateDataAssetParams arg.BasePriceMax, arg.AssetStatus, ) - var i DataAsset + var i CreateDataAssetRow err := row.Scan( &i.ID, &i.AssetName, @@ -262,7 +289,7 @@ INSERT INTO orders ( order_status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) -RETURNING id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at +RETURNING id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at, user_id ` type CreateOrderParams struct { @@ -309,6 +336,7 @@ func (q *Queries) CreateOrder(ctx context.Context, arg CreateOrderParams) (Order &i.OrderStatus, &i.OrderCreatedAt, &i.OrderUpdatedAt, + &i.UserID, ) return i, err } @@ -329,7 +357,7 @@ INSERT INTO pricing_results ( pricing_status ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) -RETURNING id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at +RETURNING id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at, agent_task_match_explanation, agent_risk_advice, agent_budget_advice, agent_next_action ` type CreatePricingResultParams struct { @@ -379,6 +407,10 @@ func (q *Queries) CreatePricingResult(ctx context.Context, arg CreatePricingResu &i.PricingStatus, &i.CreatedAt, &i.UpdatedAt, + &i.AgentTaskMatchExplanation, + &i.AgentRiskAdvice, + &i.AgentBudgetAdvice, + &i.AgentNextAction, ) return i, err } @@ -397,7 +429,7 @@ INSERT INTO validations ( validation_finished_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at +RETURNING id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at, agent_validation_explanation, agent_continue_trade_advice, agent_delivery_advice, agent_risk_notice ` type CreateValidationParams struct { @@ -440,12 +472,16 @@ func (q *Queries) CreateValidation(ctx context.Context, arg CreateValidationPara &i.ContinueRecommendation, &i.ValidationCreatedAt, &i.ValidationFinishedAt, + &i.AgentValidationExplanation, + &i.AgentContinueTradeAdvice, + &i.AgentDeliveryAdvice, + &i.AgentRiskNotice, ) return i, err } const getBuyerRequest = `-- name: GetBuyerRequest :one -SELECT id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at +SELECT id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at, user_id FROM buyer_requests WHERE id = $1 ` @@ -466,12 +502,13 @@ func (q *Queries) GetBuyerRequest(ctx context.Context, id pgtype.UUID) (BuyerReq &i.RequestStatus, &i.CreatedAt, &i.UpdatedAt, + &i.UserID, ) return i, err } const getDataAsset = `-- name: GetDataAsset :one -SELECT id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at +SELECT id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at, user_id, agent_asset_summary, agent_recommended_tasks, agent_risk_per_mission_advice, agent_asset_explanation FROM data_assets WHERE id = $1 ` @@ -503,12 +540,17 @@ func (q *Queries) GetDataAsset(ctx context.Context, id pgtype.UUID) (DataAsset, &i.AssetStatus, &i.CreatedAt, &i.UpdatedAt, + &i.UserID, + &i.AgentAssetSummary, + &i.AgentRecommendedTasks, + &i.AgentRiskPerMissionAdvice, + &i.AgentAssetExplanation, ) return i, err } const getOrder = `-- name: GetOrder :one -SELECT id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at +SELECT id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at, user_id FROM orders WHERE id = $1 ` @@ -531,12 +573,13 @@ func (q *Queries) GetOrder(ctx context.Context, id pgtype.UUID) (Order, error) { &i.OrderStatus, &i.OrderCreatedAt, &i.OrderUpdatedAt, + &i.UserID, ) return i, err } const getPricingResult = `-- name: GetPricingResult :one -SELECT id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at +SELECT id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at, agent_task_match_explanation, agent_risk_advice, agent_budget_advice, agent_next_action FROM pricing_results WHERE id = $1 ` @@ -560,12 +603,16 @@ func (q *Queries) GetPricingResult(ctx context.Context, id pgtype.UUID) (Pricing &i.PricingStatus, &i.CreatedAt, &i.UpdatedAt, + &i.AgentTaskMatchExplanation, + &i.AgentRiskAdvice, + &i.AgentBudgetAdvice, + &i.AgentNextAction, ) return i, err } const getValidation = `-- name: GetValidation :one -SELECT id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at +SELECT id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at, agent_validation_explanation, agent_continue_trade_advice, agent_delivery_advice, agent_risk_notice FROM validations WHERE id = $1 ` @@ -586,12 +633,16 @@ func (q *Queries) GetValidation(ctx context.Context, id pgtype.UUID) (Validation &i.ContinueRecommendation, &i.ValidationCreatedAt, &i.ValidationFinishedAt, + &i.AgentValidationExplanation, + &i.AgentContinueTradeAdvice, + &i.AgentDeliveryAdvice, + &i.AgentRiskNotice, ) return i, err } const listBuyerRequests = `-- name: ListBuyerRequests :many -SELECT id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at +SELECT id, asset_id, task_type, model_type, buyer_budget_min, buyer_budget_max, privacy_requirement, usage_purpose, request_note, request_status, created_at, updated_at, user_id FROM buyer_requests ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2 @@ -624,6 +675,7 @@ func (q *Queries) ListBuyerRequests(ctx context.Context, arg ListBuyerRequestsPa &i.RequestStatus, &i.CreatedAt, &i.UpdatedAt, + &i.UserID, ); err != nil { return nil, err } @@ -636,7 +688,7 @@ func (q *Queries) ListBuyerRequests(ctx context.Context, arg ListBuyerRequestsPa } const listDataAssets = `-- name: ListDataAssets :many -SELECT id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at +SELECT id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at, user_id, agent_asset_summary, agent_recommended_tasks, agent_risk_per_mission_advice, agent_asset_explanation FROM data_assets WHERE ( NULLIF($3::text, '') IS NULL @@ -714,6 +766,11 @@ func (q *Queries) ListDataAssets(ctx context.Context, arg ListDataAssetsParams) &i.AssetStatus, &i.CreatedAt, &i.UpdatedAt, + &i.UserID, + &i.AgentAssetSummary, + &i.AgentRecommendedTasks, + &i.AgentRiskPerMissionAdvice, + &i.AgentAssetExplanation, ); err != nil { return nil, err } @@ -726,7 +783,7 @@ func (q *Queries) ListDataAssets(ctx context.Context, arg ListDataAssetsParams) } const listOrders = `-- name: ListOrders :many -SELECT id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at +SELECT id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at, user_id FROM orders WHERE NULLIF($3::text, '') IS NULL OR order_status = $3::text @@ -764,6 +821,7 @@ func (q *Queries) ListOrders(ctx context.Context, arg ListOrdersParams) ([]Order &i.OrderStatus, &i.OrderCreatedAt, &i.OrderUpdatedAt, + &i.UserID, ); err != nil { return nil, err } @@ -776,7 +834,7 @@ func (q *Queries) ListOrders(ctx context.Context, arg ListOrdersParams) ([]Order } const listPricingResults = `-- name: ListPricingResults :many -SELECT id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at +SELECT id, asset_id, request_id, scenario_value_score, scenario_price_min, scenario_price_max, suggested_price, success_probability, pricing_reason_1, pricing_reason_2, pricing_reason_3, verification_suggestion, pricing_status, created_at, updated_at, agent_task_match_explanation, agent_risk_advice, agent_budget_advice, agent_next_action FROM pricing_results ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2 @@ -812,6 +870,10 @@ func (q *Queries) ListPricingResults(ctx context.Context, arg ListPricingResults &i.PricingStatus, &i.CreatedAt, &i.UpdatedAt, + &i.AgentTaskMatchExplanation, + &i.AgentRiskAdvice, + &i.AgentBudgetAdvice, + &i.AgentNextAction, ); err != nil { return nil, err } @@ -824,7 +886,7 @@ func (q *Queries) ListPricingResults(ctx context.Context, arg ListPricingResults } const listValidations = `-- name: ListValidations :many -SELECT id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at +SELECT id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at, agent_validation_explanation, agent_continue_trade_advice, agent_delivery_advice, agent_risk_notice FROM validations ORDER BY validation_created_at DESC, id DESC LIMIT $1 OFFSET $2 @@ -857,6 +919,10 @@ func (q *Queries) ListValidations(ctx context.Context, arg ListValidationsParams &i.ContinueRecommendation, &i.ValidationCreatedAt, &i.ValidationFinishedAt, + &i.AgentValidationExplanation, + &i.AgentContinueTradeAdvice, + &i.AgentDeliveryAdvice, + &i.AgentRiskNotice, ); err != nil { return nil, err } @@ -873,7 +939,7 @@ UPDATE data_assets SET asset_status = $2, updated_at = now() WHERE id = $1 -RETURNING id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at +RETURNING id, asset_name, asset_type, domain, application_scene, data_description, data_scale, collection_method, labeling_status, update_frequency, privacy_level, permission_mode, supports_validation, seller_expected_price_min, seller_expected_price_max, quality_level, scarcity_level, base_value_score, base_price_min, base_price_max, asset_status, created_at, updated_at, user_id, agent_asset_summary, agent_recommended_tasks, agent_risk_per_mission_advice, agent_asset_explanation ` type UpdateDataAssetStatusParams struct { @@ -908,6 +974,11 @@ func (q *Queries) UpdateDataAssetStatus(ctx context.Context, arg UpdateDataAsset &i.AssetStatus, &i.CreatedAt, &i.UpdatedAt, + &i.UserID, + &i.AgentAssetSummary, + &i.AgentRecommendedTasks, + &i.AgentRiskPerMissionAdvice, + &i.AgentAssetExplanation, ) return i, err } @@ -917,7 +988,7 @@ UPDATE orders SET order_status = $2, order_updated_at = now() WHERE id = $1 -RETURNING id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at +RETURNING id, asset_id, request_id, pricing_id, validation_id, asset_name, current_price, negotiation_min, negotiation_max, validation_used, delivery_mode, order_status, order_created_at, order_updated_at, user_id ` type UpdateOrderStatusParams struct { @@ -943,6 +1014,7 @@ func (q *Queries) UpdateOrderStatus(ctx context.Context, arg UpdateOrderStatusPa &i.OrderStatus, &i.OrderCreatedAt, &i.OrderUpdatedAt, + &i.UserID, ) return i, err } @@ -956,7 +1028,7 @@ SET validation_status = $2, continue_recommendation = $6, validation_finished_at = $7 WHERE id = $1 -RETURNING id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at +RETURNING id, asset_id, request_id, validation_type, validation_requested, validation_status, validation_signal, validation_score, risk_warning, continue_recommendation, validation_created_at, validation_finished_at, agent_validation_explanation, agent_continue_trade_advice, agent_delivery_advice, agent_risk_notice ` type UpdateValidationResultParams struct { @@ -993,6 +1065,10 @@ func (q *Queries) UpdateValidationResult(ctx context.Context, arg UpdateValidati &i.ContinueRecommendation, &i.ValidationCreatedAt, &i.ValidationFinishedAt, + &i.AgentValidationExplanation, + &i.AgentContinueTradeAdvice, + &i.AgentDeliveryAdvice, + &i.AgentRiskNotice, ) return i, err } diff --git a/internal/repository/models.go b/internal/repository/models.go index 59eb1f9..2d77d55 100644 --- a/internal/repository/models.go +++ b/internal/repository/models.go @@ -21,6 +21,14 @@ type BuyerRequest struct { RequestStatus string `json:"request_status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + UserID pgtype.UUID `json:"user_id"` +} + +type Config struct { + ID int32 `json:"id"` + JwtAlg string `json:"Jwt.Alg"` + JwtSignKey string `json:"Jwt.SignKey"` + JwtVerifyKye pgtype.Text `json:"Jwt.VerifyKye"` } type DataAsset struct { @@ -47,6 +55,15 @@ type DataAsset struct { AssetStatus string `json:"asset_status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + UserID pgtype.UUID `json:"user_id"` + // 智能助手对当前资产的简短画像总结 + AgentAssetSummary pgtype.Text `json:"agent_asset_summary"` + // 推荐的适用任务列表 + AgentRecommendedTasks []byte `json:"agent_recommended_tasks"` + // 当前资产更适合的权限与风险建议 + AgentRiskPerMissionAdvice pgtype.Text `json:"agent_risk_per_mission_advice"` + // 为什么该资产当前基础价值较高/中/低 + AgentAssetExplanation pgtype.Text `json:"agent_asset_explanation"` } type Order struct { @@ -64,6 +81,7 @@ type Order struct { OrderStatus string `json:"order_status"` OrderCreatedAt pgtype.Timestamptz `json:"order_created_at"` OrderUpdatedAt pgtype.Timestamptz `json:"order_updated_at"` + UserID pgtype.UUID `json:"user_id"` } type PricingResult struct { @@ -82,6 +100,25 @@ type PricingResult struct { PricingStatus string `json:"pricing_status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + // 解释当前数据与任务的匹配关系 + AgentTaskMatchExplanation pgtype.Text `json:"agent_task_match_explanation"` + // 解释当前隐私风险与权限建议 + AgentRiskAdvice pgtype.Text `json:"agent_risk_advice"` + // 根据预算给出的建议 + AgentBudgetAdvice pgtype.Text `json:"agent_budget_advice"` + // 建议接下来做什么 + AgentNextAction pgtype.Text `json:"agent_next_action"` +} + +type User struct { + ID pgtype.UUID `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + Role string `json:"role"` + DisplayName string `json:"display_name"` + AccountStatus string `json:"account_status"` + LastLoginAt pgtype.Timestamptz `json:"last_login_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` } type Validation struct { @@ -97,4 +134,12 @@ type Validation struct { ContinueRecommendation pgtype.Text `json:"continue_recommendation"` ValidationCreatedAt pgtype.Timestamptz `json:"validation_created_at"` ValidationFinishedAt pgtype.Timestamptz `json:"validation_finished_at"` + // 对当前验证结果的自然语言解释 + AgentValidationExplanation pgtype.Text `json:"agent_validation_explanation"` + // 对是否继续成交的建议 + AgentContinueTradeAdvice pgtype.Text `json:"agent_continue_trade_advice"` + // 对交付方式的建议 + AgentDeliveryAdvice pgtype.Text `json:"agent_delivery_advice"` + // 对后续交易的风险提示 + AgentRiskNotice pgtype.Text `json:"agent_risk_notice"` } diff --git a/internal/repository/user.sql.go b/internal/repository/user.sql.go new file mode 100644 index 0000000..3e30119 --- /dev/null +++ b/internal/repository/user.sql.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: user.sql + +package repository + +import ( + "context" +) + +const getUserByUsername = `-- name: GetUserByUsername :one +SELECT id, username, password, role, display_name, account_status, last_login_at, created_at +FROM users +WHERE username = $1 +` + +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { + row := q.db.QueryRow(ctx, getUserByUsername, username) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Password, + &i.Role, + &i.DisplayName, + &i.AccountStatus, + &i.LastLoginAt, + &i.CreatedAt, + ) + return i, err +} diff --git a/internal/router/router.go b/internal/router/router.go index 91df911..133e3bc 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -4,13 +4,15 @@ import ( "net/http" "time" + "gitea.starryskymeow.cn/B309/datamarket/internal/config" "gitea.starryskymeow.cn/B309/datamarket/internal/handler" "gitea.starryskymeow.cn/B309/datamarket/internal/service" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/jwtauth/v5" ) -func New(svc *service.Service) http.Handler { +func New(svc *service.Service, config *config.Config) http.Handler { r := chi.NewRouter() r.Use(middleware.RequestID) @@ -49,6 +51,15 @@ func New(svc *service.Service) http.Handler { r.Get("/validations", handler.AdminListValidations(svc)) r.Get("/orders", handler.AdminListOrders(svc)) }) + + r.Route("/auth", func(r chi.Router) { + r.Post("/login", handler.LoginHandler(svc)) + r.Group(func(r chi.Router) { + r.Use(jwtauth.Verifier(config.JWTAuth)) + r.Use(jwtauth.Authenticator(config.JWTAuth)) + r.Get("/me", handler.MeHandler(svc)) + }) + }) }) return r diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..d3ddf63 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,44 @@ +package service + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/go-chi/jwtauth/v5" + "golang.org/x/crypto/bcrypt" +) + +func (s *Service) VerifyUser(ctx context.Context, input VerifyUserInput) (VerifyUserResult, error) { + u, err := s.queries.GetUserByUsername(ctx, input.Username) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return VerifyUserResult{}, notFound("user does not exist or password is wrong") + } + return VerifyUserResult{}, internalError("auth error", nil) + } + + if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(input.Password)); err != nil { + return VerifyUserResult{}, notFound("user does not exist or password is wrong") + } + + // jwt + claims := make(map[string]any) + claims["userid"] = u.ID.String() + claims["username"] = u.Username + claims["role"] = u.Role + claims["account_status"] = u.AccountStatus + jwtauth.SetExpiryIn(claims, 24*time.Hour) + jwtauth.SetIssuedNow(claims) + + _, token, _ := s.config.JWTAuth.Encode(claims) + + return VerifyUserResult{ + Token: token, + UserId: u.ID.String(), + UserName: u.Username, + DisplayName: u.DisplayName, + Role: u.Role, + }, nil +} diff --git a/internal/service/auth_test.go b/internal/service/auth_test.go new file mode 100644 index 0000000..f1dd1c3 --- /dev/null +++ b/internal/service/auth_test.go @@ -0,0 +1,13 @@ +package service + +import ( + "log" + "testing" + + "golang.org/x/crypto/bcrypt" +) + +func TestGenerateHashedPassword(t *testing.T) { + e, _ := bcrypt.GenerateFromPassword([]byte("1145141919810"), bcrypt.DefaultCost) + log.Println(string(e)) +} diff --git a/internal/service/jwt_utils.go b/internal/service/jwt_utils.go new file mode 100644 index 0000000..6d43c33 --- /dev/null +++ b/internal/service/jwt_utils.go @@ -0,0 +1 @@ +package service diff --git a/internal/service/service.go b/internal/service/service.go index 3b3e510..fbc26da 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -2,13 +2,16 @@ package service import ( "context" + "encoding/json" "errors" "fmt" + "log/slog" "math" "strconv" "strings" "time" + "gitea.starryskymeow.cn/B309/datamarket/internal/config" "gitea.starryskymeow.cn/B309/datamarket/internal/repository" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -37,12 +40,14 @@ var ( type Service struct { queries *repository.Queries + config *config.Config now func() time.Time } -func New(queries *repository.Queries) *Service { +func New(queries *repository.Queries, cfg *config.Config) *Service { return &Service{ queries: queries, + config: cfg, now: time.Now, } } @@ -884,29 +889,33 @@ func timestamptzValue(value time.Time) pgtype.Timestamptz { func toAsset(asset repository.DataAsset) Asset { return Asset{ - ID: asset.ID.String(), - AssetName: asset.AssetName, - AssetType: asset.AssetType, - Domain: asset.Domain, - ApplicationScene: toTextPointer(asset.ApplicationScene), - DataDescription: asset.DataDescription, - DataScale: asset.DataScale, - CollectionMethod: asset.CollectionMethod, - LabelingStatus: toTextPointer(asset.LabelingStatus), - UpdateFrequency: toTextPointer(asset.UpdateFrequency), - PrivacyLevel: asset.PrivacyLevel, - PermissionMode: asset.PermissionMode, - SupportsValidation: asset.SupportsValidation, - SellerExpectedPriceMin: toNumericPointer(asset.SellerExpectedPriceMin), - SellerExpectedPriceMax: toNumericPointer(asset.SellerExpectedPriceMax), - QualityLevel: toTextPointer(asset.QualityLevel), - ScarcityLevel: toTextPointer(asset.ScarcityLevel), - BaseValueScore: toNumericPointer(asset.BaseValueScore), - BasePriceMin: toNumericPointer(asset.BasePriceMin), - BasePriceMax: toNumericPointer(asset.BasePriceMax), - AssetStatus: asset.AssetStatus, - CreatedAt: toTimePointer(asset.CreatedAt), - UpdatedAt: toTimePointer(asset.UpdatedAt), + ID: asset.ID.String(), + AssetName: asset.AssetName, + AssetType: asset.AssetType, + Domain: asset.Domain, + ApplicationScene: toTextPointer(asset.ApplicationScene), + DataDescription: asset.DataDescription, + DataScale: asset.DataScale, + CollectionMethod: asset.CollectionMethod, + LabelingStatus: toTextPointer(asset.LabelingStatus), + UpdateFrequency: toTextPointer(asset.UpdateFrequency), + PrivacyLevel: asset.PrivacyLevel, + PermissionMode: asset.PermissionMode, + SupportsValidation: asset.SupportsValidation, + SellerExpectedPriceMin: toNumericPointer(asset.SellerExpectedPriceMin), + SellerExpectedPriceMax: toNumericPointer(asset.SellerExpectedPriceMax), + QualityLevel: toTextPointer(asset.QualityLevel), + ScarcityLevel: toTextPointer(asset.ScarcityLevel), + BaseValueScore: toNumericPointer(asset.BaseValueScore), + BasePriceMin: toNumericPointer(asset.BasePriceMin), + BasePriceMax: toNumericPointer(asset.BasePriceMax), + AssetStatus: asset.AssetStatus, + CreatedAt: toTimePointer(asset.CreatedAt), + UpdatedAt: toTimePointer(asset.UpdatedAt), + AgentAssetSummary: toTextPointer(asset.AgentAssetSummary), + AgentRecommendedTasks: toStringArray(asset.AgentRecommendedTasks), + AgentRiskPerMissionAdvice: toTextPointer(asset.AgentRiskPerMissionAdvice), + AgentAssetExplanation: toTextPointer(asset.AgentAssetExplanation), } } @@ -979,6 +988,14 @@ func toTextPointer(value pgtype.Text) *string { return new(value.String) } +func toStringArray(value []byte) []string { + var result []string + if err := json.Unmarshal(value, &result); err != nil { + slog.Warn(err.Error(), "value", string(value)) + } + return result +} + func toNumericPointer(value pgtype.Numeric) *float64 { if !value.Valid { return nil diff --git a/internal/service/types.go b/internal/service/types.go index f49e6da..8593a25 100644 --- a/internal/service/types.go +++ b/internal/service/types.go @@ -36,6 +36,14 @@ type Asset struct { AssetStatus string `json:"asset_status"` CreatedAt *time.Time `json:"created_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"` + // 智能助手对当前资产的简短画像总结 + AgentAssetSummary *string `json:"agent_asset_summary"` + // 推荐的适用任务列表 + AgentRecommendedTasks []string `json:"agent_recommended_tasks"` + // 当前资产更适合的权限与风险建议 + AgentRiskPerMissionAdvice *string `json:"agent_risk_per_mission_advice"` + // 为什么该资产当前基础价值较高/中/低 + AgentAssetExplanation *string `json:"agent_asset_explanation"` } type AssetCreateInput struct { @@ -176,17 +184,20 @@ type AssetService interface { GetAsset(context.Context, string) (Asset, error) ListAssets(context.Context, AssetListInput) (ListResult[Asset], error) UpdateAssetStatus(context.Context, string, StatusUpdate) (AssetStatusResult, error) + //DeleteAsset(context.Context, string) (AssetStatusResult, error) } type PricingService interface { CreatePricing(context.Context, PricingCreateInput) (Pricing, error) GetPricing(context.Context, string) (Pricing, error) + //DeletePricing(context.Context, string) (Pricing, error) } type ValidationService interface { CreateValidation(context.Context, ValidationCreateInput) (ValidationCreateResult, error) GetValidation(context.Context, string) (Validation, error) ListValidations(context.Context, ValidationListInput) (ListResult[Validation], error) + //DeleteValidation(context.Context, string) (Validation, error) } type OrderService interface { @@ -194,4 +205,22 @@ type OrderService interface { GetOrder(context.Context, string) (Order, error) ListOrders(context.Context, OrderListInput) (ListResult[Order], error) UpdateOrderStatus(context.Context, string, StatusUpdate) (OrderCreateResult, error) + //DeleteOrder(context.Context, string) (Order, error) +} + +type VerifyUserInput struct { + Username string + Password string +} + +type VerifyUserResult struct { + Token string `json:"token"` + UserId string `json:"user_id"` + UserName string `json:"user_name"` + DisplayName string `json:"display_name"` + Role string `json:"role"` +} + +type AuthService interface { + VerifyUser(context.Context, VerifyUserInput) (VerifyUserResult, error) }