后端:公告√

注意数据库新建notice表
This commit is contained in:
2025-09-16 20:03:17 +08:00
parent 158b3e65b6
commit 562ec4abf9
26 changed files with 1468 additions and 35 deletions

566
backend/db/db.sql Normal file
View File

@@ -0,0 +1,566 @@
-- =====================================================================
-- 配件查询 App 数据库结构MySQL 8.0
-- 依据:/doc/requirements.md、/doc/functional_spec.md、/doc/architecture.md
-- 注意:不在此文件中创建数据库与用户,请在外部以环境配置完成
-- 字符集统一为 utf8mb4排序规则 utf8mb4_0900_ai_ci
-- =====================================================================
SET NAMES utf8mb4;
SET time_zone = '+00:00';
SET sql_mode = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
SET FOREIGN_KEY_CHECKS = 0;
-- =====================================================================
-- 基础:租户与用户
-- =====================================================================
CREATE TABLE IF NOT EXISTS shops (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '店铺/租户ID',
name VARCHAR(100) NOT NULL COMMENT '店铺名称',
status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用 0停用',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_shops_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='店铺/租户';
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
shop_id BIGINT UNSIGNED NOT NULL COMMENT '所属店铺',
phone VARCHAR(32) NULL COMMENT '手机号',
name VARCHAR(64) NOT NULL COMMENT '姓名',
role VARCHAR(32) NOT NULL DEFAULT 'staff' COMMENT '角色owner/staff/finance/...',
password_hash VARCHAR(255) NULL COMMENT '密码哈希(若采用短信登录可为空)',
status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态1启用 0停用',
is_owner TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否店主',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_users_shop_phone (shop_id, phone),
KEY idx_users_shop (shop_id),
CONSTRAINT fk_users_shop FOREIGN KEY (shop_id) REFERENCES shops(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
-- 第三方身份映射(微信)
CREATE TABLE IF NOT EXISTS user_identities (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
provider ENUM('wechat_mp','wechat_app') NOT NULL COMMENT '身份提供方:小程序/APP',
openid VARCHAR(64) NOT NULL,
unionid VARCHAR(64) NULL,
nickname VARCHAR(64) NULL,
avatar_url VARCHAR(512) NULL,
last_login_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_identity_provider_openid (provider, openid),
UNIQUE KEY ux_identity_unionid (unionid),
KEY idx_identity_user (user_id),
KEY idx_identity_shop (shop_id),
CONSTRAINT fk_identity_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_identity_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='第三方身份映射(微信)';
-- 微信会话(小程序/APP 临时会话)
CREATE TABLE IF NOT EXISTS wechat_sessions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
provider ENUM('wechat_mp','wechat_app') NOT NULL,
openid VARCHAR(64) NOT NULL,
session_key VARCHAR(128) NOT NULL,
expires_at DATETIME NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_wechat_session (provider, openid),
KEY idx_wechat_session_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='微信会话(临时)';
CREATE TABLE IF NOT EXISTS system_parameters (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL COMMENT '创建/最后修改人',
`key` VARCHAR(64) NOT NULL COMMENT '参数键',
`value` JSON NOT NULL COMMENT '参数值JSON',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_sysparams_shop_key (shop_id, `key`),
KEY idx_sysparams_shop (shop_id),
CONSTRAINT fk_sysparams_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_sysparams_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统参数(租户级)';
-- =====================================================================
-- 货品域(含价格/库存/图片/别名)
-- =====================================================================
CREATE TABLE IF NOT EXISTS product_categories (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(64) NOT NULL,
parent_id BIGINT UNSIGNED NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_categories_shop_name (shop_id, name),
KEY idx_categories_shop (shop_id),
KEY idx_categories_parent (parent_id),
CONSTRAINT fk_categories_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_categories_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_categories_parent FOREIGN KEY (parent_id) REFERENCES product_categories(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品类别';
CREATE TABLE IF NOT EXISTS product_units (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(16) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_units_shop_name (shop_id, name),
KEY idx_units_shop (shop_id),
CONSTRAINT fk_units_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_units_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品单位';
CREATE TABLE IF NOT EXISTS global_skus (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(120) NOT NULL COMMENT 'SKU名称',
brand VARCHAR(64) NULL,
model VARCHAR(64) NULL,
spec VARCHAR(128) NULL,
barcode VARCHAR(32) NULL,
unit_id BIGINT UNSIGNED NULL,
tags JSON NULL,
status ENUM('published','offline') NOT NULL DEFAULT 'published',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_global_skus_barcode (barcode),
KEY idx_global_skus_brand_model (brand, model),
CONSTRAINT fk_globalsku_unit FOREIGN KEY (unit_id) REFERENCES product_units(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='全局SKU众包';
CREATE TABLE IF NOT EXISTS products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(120) NOT NULL,
category_id BIGINT UNSIGNED NULL,
unit_id BIGINT UNSIGNED NOT NULL,
brand VARCHAR(64) NULL,
model VARCHAR(64) NULL,
spec VARCHAR(128) NULL,
origin VARCHAR(64) NULL,
barcode VARCHAR(32) NULL,
alias VARCHAR(120) NULL,
description TEXT NULL,
global_sku_id BIGINT UNSIGNED NULL,
safe_min DECIMAL(18,3) NULL,
safe_max DECIMAL(18,3) NULL,
search_text TEXT NULL COMMENT '供全文检索的聚合字段(名称/品牌/型号/规格/别名)',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_products_shop_barcode (shop_id, barcode),
KEY idx_products_shop (shop_id),
KEY idx_products_category (category_id),
KEY idx_products_unit (unit_id),
FULLTEXT KEY ft_products_search (name, brand, model, spec, search_text),
CONSTRAINT fk_products_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_products_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES product_categories(id),
CONSTRAINT fk_products_unit FOREIGN KEY (unit_id) REFERENCES product_units(id),
CONSTRAINT fk_products_globalsku FOREIGN KEY (global_sku_id) REFERENCES global_skus(id),
CONSTRAINT ck_products_safe_range CHECK (safe_min IS NULL OR safe_max IS NULL OR safe_min <= safe_max)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品';
CREATE TABLE IF NOT EXISTS product_aliases (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
alias VARCHAR(120) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_product_alias (product_id, alias),
KEY idx_product_alias_product (product_id),
CONSTRAINT fk_alias_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_alias_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_alias_product FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品别名';
CREATE TABLE IF NOT EXISTS product_prices (
product_id BIGINT UNSIGNED NOT NULL,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
purchase_price DECIMAL(18,2) NOT NULL DEFAULT 0,
retail_price DECIMAL(18,2) NOT NULL DEFAULT 0,
distribution_price DECIMAL(18,2) NOT NULL DEFAULT 0,
wholesale_price DECIMAL(18,2) NOT NULL DEFAULT 0,
big_client_price DECIMAL(18,2) NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (product_id),
KEY idx_prices_shop (shop_id),
CONSTRAINT fk_prices_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
CONSTRAINT fk_prices_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_prices_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT ck_prices_non_negative CHECK (
purchase_price >= 0 AND retail_price >= 0 AND distribution_price >= 0 AND wholesale_price >= 0 AND big_client_price >= 0
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品价格(含四列销售价)';
CREATE TABLE IF NOT EXISTS inventories (
product_id BIGINT UNSIGNED NOT NULL,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
quantity DECIMAL(18,3) NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (product_id),
KEY idx_inventories_shop (shop_id),
CONSTRAINT fk_inv_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
CONSTRAINT fk_inv_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_inv_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT ck_inv_qty_non_negative CHECK (quantity >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库存';
CREATE TABLE IF NOT EXISTS product_images (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
url VARCHAR(512) NOT NULL,
hash VARCHAR(64) NULL COMMENT '内容哈希(去重)',
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_product_image_hash (product_id, hash),
KEY idx_product_images_product (product_id),
CONSTRAINT fk_pimg_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_pimg_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_pimg_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品图片';
-- =====================================================================
-- 往来单位与账户
-- =====================================================================
CREATE TABLE IF NOT EXISTS customers (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(120) NOT NULL,
phone VARCHAR(32) NULL,
level VARCHAR(32) NULL COMMENT '客户等级标签',
price_level ENUM('retail','distribution','wholesale','big_client') NOT NULL DEFAULT 'retail' COMMENT '默认售价列',
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
remark VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_customers_shop (shop_id),
KEY idx_customers_phone (phone),
CONSTRAINT fk_customers_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_customers_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户';
CREATE TABLE IF NOT EXISTS suppliers (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(120) NOT NULL,
phone VARCHAR(32) NULL,
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
remark VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_suppliers_shop (shop_id),
KEY idx_suppliers_phone (phone),
CONSTRAINT fk_suppliers_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_suppliers_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='供应商';
CREATE TABLE IF NOT EXISTS accounts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(64) NOT NULL,
`type` ENUM('cash','bank','alipay','wechat','other') NOT NULL DEFAULT 'cash',
balance DECIMAL(18,2) NOT NULL DEFAULT 0,
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_accounts_shop_name (shop_id, name),
KEY idx_accounts_shop (shop_id),
CONSTRAINT fk_accounts_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_accounts_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='结算账户';
-- =====================================================================
-- 单据域(销售/进货/其他收支/流水)
-- =====================================================================
CREATE TABLE IF NOT EXISTS sales_orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL COMMENT '创建人',
customer_id BIGINT UNSIGNED NULL,
order_no VARCHAR(32) NOT NULL,
order_time DATETIME NOT NULL,
status ENUM('draft','approved','returned','void') NOT NULL DEFAULT 'draft',
amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '应收合计',
paid_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '已收合计',
remark VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_sales_order_no (shop_id, order_no),
KEY idx_sales_shop_time (shop_id, order_time),
KEY idx_sales_customer (customer_id),
CONSTRAINT fk_sales_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_sales_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_sales_customer FOREIGN KEY (customer_id) REFERENCES customers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='销售单';
CREATE TABLE IF NOT EXISTS sales_order_items (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
quantity DECIMAL(18,3) NOT NULL,
unit_price DECIMAL(18,2) NOT NULL,
discount_rate DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '折扣百分比0-100',
amount DECIMAL(18,2) NOT NULL,
PRIMARY KEY (id),
KEY idx_soi_order (order_id),
KEY idx_soi_product (product_id),
CONSTRAINT fk_soi_order FOREIGN KEY (order_id) REFERENCES sales_orders(id) ON DELETE CASCADE,
CONSTRAINT fk_soi_product FOREIGN KEY (product_id) REFERENCES products(id),
CONSTRAINT ck_soi_qty CHECK (quantity > 0),
CONSTRAINT ck_soi_price CHECK (unit_price >= 0),
CONSTRAINT ck_soi_discount CHECK (discount_rate >= 0 AND discount_rate <= 100)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='销售单明细';
CREATE TABLE IF NOT EXISTS purchase_orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
supplier_id BIGINT UNSIGNED NULL,
order_no VARCHAR(32) NOT NULL,
order_time DATETIME NOT NULL,
status ENUM('draft','approved','void') NOT NULL DEFAULT 'draft',
amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '应付合计',
paid_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '已付合计',
remark VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_purchase_order_no (shop_id, order_no),
KEY idx_purchase_shop_time (shop_id, order_time),
KEY idx_purchase_supplier (supplier_id),
CONSTRAINT fk_purchase_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_purchase_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_purchase_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='进货单';
CREATE TABLE IF NOT EXISTS purchase_order_items (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
quantity DECIMAL(18,3) NOT NULL,
unit_price DECIMAL(18,2) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
PRIMARY KEY (id),
KEY idx_poi_order (order_id),
KEY idx_poi_product (product_id),
CONSTRAINT fk_poi_order FOREIGN KEY (order_id) REFERENCES purchase_orders(id) ON DELETE CASCADE,
CONSTRAINT fk_poi_product FOREIGN KEY (product_id) REFERENCES products(id),
CONSTRAINT ck_poi_qty CHECK (quantity > 0),
CONSTRAINT ck_poi_price CHECK (unit_price >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='进货单明细';
CREATE TABLE IF NOT EXISTS payments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
biz_type ENUM('sale','purchase','other') NOT NULL,
biz_id BIGINT UNSIGNED NULL COMMENT '业务表IDsales_orders/purchase_orders/other_transactions',
account_id BIGINT UNSIGNED NOT NULL,
direction ENUM('in','out') NOT NULL COMMENT '收款/付款',
amount DECIMAL(18,2) NOT NULL,
pay_time DATETIME NOT NULL,
remark VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_payments_shop_time (shop_id, pay_time),
KEY idx_payments_biz (biz_type, biz_id),
CONSTRAINT fk_payments_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_payments_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_payments_account FOREIGN KEY (account_id) REFERENCES accounts(id),
CONSTRAINT ck_payments_amount CHECK (amount > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='收付款记录';
CREATE TABLE IF NOT EXISTS other_transactions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
`type` ENUM('income','expense') NOT NULL,
category VARCHAR(64) NOT NULL,
counterparty_type VARCHAR(32) NULL COMMENT 'customer/supplier/other',
counterparty_id BIGINT UNSIGNED NULL,
account_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(18,2) NOT NULL,
tx_time DATETIME NOT NULL,
remark VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_ot_shop_time (shop_id, tx_time),
KEY idx_ot_account (account_id),
CONSTRAINT fk_ot_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_ot_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_ot_account FOREIGN KEY (account_id) REFERENCES accounts(id),
CONSTRAINT ck_ot_amount CHECK (amount > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='其他收入/支出';
-- =====================================================================
-- 配件查询与审核、附件
-- =====================================================================
CREATE TABLE IF NOT EXISTS part_submissions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
model_unique VARCHAR(128) NOT NULL COMMENT '型号(唯一)',
brand VARCHAR(64) NULL,
spec VARCHAR(128) NULL,
size VARCHAR(64) NULL,
aperture VARCHAR(64) NULL,
compatible TEXT NULL COMMENT '适配信息',
status ENUM('draft','pending','rejected','published') NOT NULL DEFAULT 'pending',
reason VARCHAR(255) NULL COMMENT '驳回原因',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_part_model_unique (model_unique),
KEY idx_part_submissions_shop (shop_id),
CONSTRAINT fk_part_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_part_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='配件数据提交(审核)';
CREATE TABLE IF NOT EXISTS attachments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NULL COMMENT '全局资源可空,本地资源属于租户',
user_id BIGINT UNSIGNED NULL,
owner_type VARCHAR(32) NOT NULL COMMENT '资源归属类型product/part_submission/global_sku/...',
owner_id BIGINT UNSIGNED NOT NULL,
url VARCHAR(512) NOT NULL,
hash VARCHAR(64) NULL,
meta JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_attachments_hash (hash),
KEY idx_attachments_owner (owner_type, owner_id),
CONSTRAINT fk_att_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_att_user FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='通用附件(图片等)';
-- =====================================================================
-- 触发器:维护 products.search_text 聚合字段
-- =====================================================================
DROP TRIGGER IF EXISTS trg_products_ai;
DELIMITER $$
CREATE TRIGGER trg_products_ai AFTER INSERT ON products
FOR EACH ROW
BEGIN
UPDATE products
SET search_text = CONCAT_WS(' ', NEW.name, NEW.brand, NEW.model, NEW.spec)
WHERE id = NEW.id;
END $$
DELIMITER ;
DROP TRIGGER IF EXISTS trg_products_au;
DELIMITER $$
CREATE TRIGGER trg_products_au BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
SET NEW.search_text = CONCAT_WS(' ', NEW.name, NEW.brand, NEW.model, NEW.spec);
END $$
DELIMITER ;
-- 当别名变化时重建 search_text名称/品牌/型号/规格 + 所有别名)
DROP TRIGGER IF EXISTS trg_palias_ai;
DELIMITER $$
CREATE TRIGGER trg_palias_ai AFTER INSERT ON product_aliases
FOR EACH ROW
BEGIN
UPDATE products p
JOIN (
SELECT pa.product_id, GROUP_CONCAT(pa.alias SEPARATOR ' ') AS aliases
FROM product_aliases pa
WHERE pa.product_id = NEW.product_id AND pa.deleted_at IS NULL
GROUP BY pa.product_id
) a ON a.product_id = p.id
SET p.search_text = CONCAT_WS(' ', p.name, p.brand, p.model, p.spec, a.aliases)
WHERE p.id = NEW.product_id;
END $$
DELIMITER ;
DROP TRIGGER IF EXISTS trg_palias_au;
DELIMITER $$
CREATE TRIGGER trg_palias_au AFTER UPDATE ON product_aliases
FOR EACH ROW
BEGIN
UPDATE products p
JOIN (
SELECT pa.product_id, GROUP_CONCAT(pa.alias SEPARATOR ' ') AS aliases
FROM product_aliases pa
WHERE pa.product_id = NEW.product_id AND pa.deleted_at IS NULL
GROUP BY pa.product_id
) a ON a.product_id = p.id
SET p.search_text = CONCAT_WS(' ', p.name, p.brand, p.model, p.spec, a.aliases)
WHERE p.id = NEW.product_id;
END $$
DELIMITER ;
DROP TRIGGER IF EXISTS trg_palias_ad;
DELIMITER $$
CREATE TRIGGER trg_palias_ad AFTER DELETE ON product_aliases
FOR EACH ROW
BEGIN
UPDATE products p
LEFT JOIN (
SELECT pa.product_id, GROUP_CONCAT(pa.alias SEPARATOR ' ') AS aliases
FROM product_aliases pa
WHERE pa.product_id = OLD.product_id AND pa.deleted_at IS NULL
GROUP BY pa.product_id
) a ON a.product_id = p.id
SET p.search_text = CONCAT_WS(' ', p.name, p.brand, p.model, p.spec, COALESCE(a.aliases, ''))
WHERE p.id = OLD.product_id;
END $$
DELIMITER ;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,39 @@
## 前后端数据库状态说明
**更新日期**: 2025-09-16
### 概要
- 数据库已落地:已在远程 MySQL `mysql.tonaspace.com``partsinquiry` 库完成初始化(表结构与触发器已创建)。
- 已生成根目录文档:`/doc/database_documentation.md` 已同步线上结构(字段、索引、外键、触发器)。
- 后端代码仍未配置数据源依赖与连接,前端无本地结构化存储方案。
### 已建库与连接信息(用于部署/联调)
- Address: `mysql.tonaspace.com`
- Database: `partsinquiry`
- User: `root`
- 说明:所有结构变更均通过 MysqlMCP 执行并已落地到线上库。
### 后端Spring Boot数据库状态
- 依赖:`pom.xml` 未包含 `spring-boot-starter-web``spring-boot-starter-data-jpa``mysql-connector-j` 等数据库相关依赖。
- 配置:`src/main/resources/application.properties` 仅有 `spring.application.name=demo`;未配置 `spring.datasource.*``spring.jpa.*`
- 数据模型:`src/main/java` 未发现 `@Entity`、Repository、Service存在 `backend/db/db.sql` 脚本,已执行至远程库。
- 迁移:未发现 Flyway/Liquibase 配置与脚本(当前通过 MysqlMCP 手工执行)。
- 结论:数据库已初始化,但后端未配置运行时数据源与接口,暂不可用。
### 前端uni-app数据库状态
- 数据持久化:未见 IndexedDB/WebSQL/SQLite/云数据库使用;页面数据为内置静态数据。
- 本地存储:未见 `uni.setStorage`/`uni.getStorage` 的集中封装或结构化键空间设计。
- 结论:前端当前不涉及本地数据库或结构化存储方案。
### 风险与影响
- 后端未配置数据源与接口,应用无法读写远端库(虽已建表)。
- 无接口契约,前后端仍无法联调涉及数据库的功能。
### 建议的后续行动(不自动执行)
- 在后端引入依赖:`spring-boot-starter-web``spring-boot-starter-data-jpa``mysql-connector-j`
- 配置数据源:使用环境变量注入 `SPRING_DATASOURCE_URL``SPRING_DATASOURCE_USERNAME``SPRING_DATASOURCE_PASSWORD` 等,指向上述远程库。
- 引入迁移工具Flyway/Liquibase管理 DDL后续所有变更继续通过 MysqlMCP 执行,并同步 `/doc/database_documentation.md`
- 增加健康检查与基础 CRUD 接口;在 `/doc/openapi.yaml` 按规范登记并标注实现状态(❌/✅)。

View File

@@ -35,6 +35,25 @@
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Web MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA for MySQL access -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -0,0 +1,82 @@
package com.example.demo.notice;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "notices")
public class Notice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "shop_id", nullable = false)
private Long shopId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "title", nullable = false, length = 120)
private String title;
@Column(name = "content", nullable = false, length = 500)
private String content;
@Column(name = "tag", length = 32)
private String tag;
@Column(name = "is_pinned", nullable = false)
private Boolean pinned = false;
@Column(name = "starts_at")
private LocalDateTime startsAt;
@Column(name = "ends_at")
private LocalDateTime endsAt;
@Convert(converter = NoticeStatusConverter.class)
@Column(name = "status", nullable = false, length = 16)
private NoticeStatus status = NoticeStatus.PUBLISHED;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getTag() { return tag; }
public void setTag(String tag) { this.tag = tag; }
public Boolean getPinned() { return pinned; }
public void setPinned(Boolean pinned) { this.pinned = pinned; }
public LocalDateTime getStartsAt() { return startsAt; }
public void setStartsAt(LocalDateTime startsAt) { this.startsAt = startsAt; }
public LocalDateTime getEndsAt() { return endsAt; }
public void setEndsAt(LocalDateTime endsAt) { this.endsAt = endsAt; }
public NoticeStatus getStatus() { return status; }
public void setStatus(NoticeStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}

View File

@@ -0,0 +1,31 @@
package com.example.demo.notice;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/notices")
public class NoticeController {
private final NoticeService noticeService;
public NoticeController(NoticeService noticeService) {
this.noticeService = noticeService;
}
/**
* 简化:通过请求头 X-Shop-Id 传递当前店铺ID。
*/
@GetMapping
public ResponseEntity<List<Notice>> list(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
Long sid = (shopId == null ? 1L : shopId);
return ResponseEntity.ok(noticeService.listActive(sid));
}
}

View File

@@ -0,0 +1,17 @@
package com.example.demo.notice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface NoticeRepository extends JpaRepository<Notice, Long> {
@Query("SELECT n FROM Notice n WHERE n.shopId = :shopId AND n.status = :status " +
"AND (n.startsAt IS NULL OR n.startsAt <= CURRENT_TIMESTAMP) AND (n.endsAt IS NULL OR n.endsAt >= CURRENT_TIMESTAMP) " +
"ORDER BY n.pinned DESC, n.createdAt DESC")
List<Notice> findActiveNotices(@Param("shopId") Long shopId, @Param("status") NoticeStatus status);
}

View File

@@ -0,0 +1,20 @@
package com.example.demo.notice;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class NoticeService {
private final NoticeRepository noticeRepository;
public NoticeService(NoticeRepository noticeRepository) {
this.noticeRepository = noticeRepository;
}
public List<Notice> listActive(Long shopId) {
return noticeRepository.findActiveNotices(shopId, NoticeStatus.PUBLISHED);
}
}

View File

@@ -0,0 +1,12 @@
package com.example.demo.notice;
/**
* 公告状态。
*/
public enum NoticeStatus {
DRAFT,
PUBLISHED,
OFFLINE
}

View File

@@ -0,0 +1,30 @@
package com.example.demo.notice;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter(autoApply = false)
public class NoticeStatusConverter implements AttributeConverter<NoticeStatus, String> {
@Override
public String convertToDatabaseColumn(NoticeStatus attribute) {
if (attribute == null) return null;
return switch (attribute) {
case DRAFT -> "draft";
case PUBLISHED -> "published";
case OFFLINE -> "offline";
};
}
@Override
public NoticeStatus convertToEntityAttribute(String dbData) {
if (dbData == null) return null;
return switch (dbData) {
case "draft" -> NoticeStatus.DRAFT;
case "published" -> NoticeStatus.PUBLISHED;
case "offline" -> NoticeStatus.OFFLINE;
default -> NoticeStatus.PUBLISHED;
};
}
}

View File

@@ -1 +1,25 @@
spring.application.name=demo
# 数据源配置(通过环境变量注入,避免硬编码)
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
# JPA 基本配置
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# CORS 简单放开(如需跨域)
spring.web.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*}
spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
spring.web.cors.allowed-headers=*
# Hikari 连接池保活(避免云数据库空闲断开)
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.keepalive-time=300000
spring.datasource.hikari.connection-timeout=30000

View File

@@ -0,0 +1,401 @@
## partsinquiry 数据库文档
更新日期2025-09-16
说明:本文件根据远程库 mysql.tonaspace.com 中 `partsinquiry` 的实际结构生成,字段/索引/外键信息以线上为准。
### shops
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 店铺/租户ID |
| name | VARCHAR(100) | NOT NULL | | 店铺名称 |
| status | TINYINT UNSIGNED | NOT NULL | 1 | 状态1启用 0停用 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_shops_status` (`status`)
### users
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 用户ID |
| shop_id | BIGINT UNSIGNED | NOT NULL | | 所属店铺 |
| phone | VARCHAR(32) | YES | | 手机号 |
| name | VARCHAR(64) | NOT NULL | | 姓名 |
| role | VARCHAR(32) | NOT NULL | staff | 角色owner/staff/finance/... |
| password_hash | VARCHAR(255) | YES | | 密码哈希(若采用短信登录可为空) |
| status | TINYINT UNSIGNED | NOT NULL | 1 | 状态1启用 0停用 |
| is_owner | TINYINT(1) | NOT NULL | 0 | 是否店主 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_users_shop` (`shop_id`) - UNIQUE: `ux_users_shop_phone` (`shop_id`,`phone`)
**Foreign Keys**: - `fk_users_shop`: `shop_id``shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
### user_identities
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| provider | ENUM('wechat_mp','wechat_app') | NOT NULL | | 身份提供方:小程序/APP |
| openid | VARCHAR(64) | NOT NULL | | |
| unionid | VARCHAR(64) | YES | | |
| nickname | VARCHAR(64) | YES | | |
| avatar_url | VARCHAR(512) | YES | | |
| last_login_at | DATETIME | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_identity_shop` (`shop_id`) - KEY: `idx_identity_user` (`user_id`) - UNIQUE: `ux_identity_provider_openid` (`provider`,`openid`) - UNIQUE: `ux_identity_unionid` (`unionid`)
**Foreign Keys**: - `fk_identity_shop`: `shop_id``shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_identity_user`: `user_id``users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
### wechat_sessions
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| provider | ENUM('wechat_mp','wechat_app') | NOT NULL | | |
| openid | VARCHAR(64) | NOT NULL | | |
| session_key | VARCHAR(128) | NOT NULL | | |
| expires_at | DATETIME | NOT NULL | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_wechat_session_expires` (`expires_at`) - UNIQUE: `ux_wechat_session` (`provider`,`openid`)
### system_parameters
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | 创建/最后修改人 |
| key | VARCHAR(64) | NOT NULL | | 参数键 |
| value | JSON | NOT NULL | | 参数值JSON |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sysparams_shop` (`shop_id`) - UNIQUE: `ux_sysparams_shop_key` (`shop_id`,`key`)
**Foreign Keys**: - `fk_sysparams_shop`: `shop_id``shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_sysparams_user`: `user_id``users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
### product_units
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(16) | NOT NULL | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_units_shop` (`shop_id`) - UNIQUE: `ux_units_shop_name` (`shop_id`,`name`)
**Foreign Keys**: - `fk_units_shop`: `shop_id``shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_units_user`: `user_id``users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
### global_skus
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| name | VARCHAR(120) | NOT NULL | | SKU名称 |
| brand | VARCHAR(64) | YES | | |
| model | VARCHAR(64) | YES | | |
| spec | VARCHAR(128) | YES | | |
| barcode | VARCHAR(32) | YES | | |
| unit_id | BIGINT UNSIGNED | YES | | |
| tags | JSON | YES | | |
| status | ENUM('published','offline') | NOT NULL | published | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_global_skus_brand_model` (`brand`,`model`) - UNIQUE: `ux_global_skus_barcode` (`barcode`)
**Foreign Keys**: - `fk_globalsku_unit`: `unit_id``product_units(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
### product_categories
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(64) | NOT NULL | | |
| parent_id | BIGINT UNSIGNED | YES | | |
| sort_order | INT | NOT NULL | 0 | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_categories_shop` (`shop_id`) - KEY: `idx_categories_parent` (`parent_id`) - UNIQUE: `ux_categories_shop_name` (`shop_id`,`name`)
**Foreign Keys**: - `fk_categories_shop`: `shop_id``shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_categories_user`: `user_id``users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_categories_parent`: `parent_id``product_categories(id)` ON UPDATE NO ACTION ON DELETE NO ACTION
### products
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(120) | NOT NULL | | 供全文检索 |
| category_id | BIGINT UNSIGNED | YES | | |
| unit_id | BIGINT UNSIGNED | NOT NULL | | |
| brand | VARCHAR(64) | YES | | |
| model | VARCHAR(64) | YES | | |
| spec | VARCHAR(128) | YES | | |
| origin | VARCHAR(64) | YES | | |
| barcode | VARCHAR(32) | YES | | |
| alias | VARCHAR(120) | YES | | |
| description | TEXT | YES | | |
| global_sku_id | BIGINT UNSIGNED | YES | | |
| safe_min | DECIMAL(18,3) | YES | | |
| safe_max | DECIMAL(18,3) | YES | | |
| search_text | TEXT | YES | | 供全文检索的聚合字段(名称/品牌/型号/规格/别名) |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_unit` (`unit_id`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`)
**Foreign Keys**: - `fk_products_shop`: `shop_id``shops(id)` - `fk_products_user`: `user_id``users(id)` - `fk_products_category`: `category_id``product_categories(id)` - `fk_products_unit`: `unit_id``product_units(id)` - `fk_products_globalsku`: `global_sku_id``global_skus(id)`
### product_aliases
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| alias | VARCHAR(120) | NOT NULL | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_alias_product` (`product_id`) - UNIQUE: `ux_product_alias` (`product_id`,`alias`)
**Foreign Keys**: - `fk_alias_shop`: `shop_id``shops(id)` - `fk_alias_user`: `user_id``users(id)` - `fk_alias_product`: `product_id``products(id)`
### product_images
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| url | VARCHAR(512) | NOT NULL | | |
| hash | VARCHAR(64) | YES | | 内容哈希(去重) |
| sort_order | INT | NOT NULL | 0 | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_images_product` (`product_id`) - UNIQUE: `ux_product_image_hash` (`product_id`,`hash`)
**Foreign Keys**: - `fk_pimg_shop`: `shop_id``shops(id)` - `fk_pimg_user`: `user_id``users(id)` - `fk_pimg_product`: `product_id``products(id)` ON DELETE CASCADE
### product_prices
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| purchase_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
| retail_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
| distribution_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
| wholesale_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
| big_client_price | DECIMAL(18,2) | NOT NULL | 0.00 | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `product_id` - KEY: `idx_prices_shop` (`shop_id`)
**Foreign Keys**: - `fk_prices_product`: `product_id``products(id)` ON DELETE CASCADE - `fk_prices_shop`: `shop_id``shops(id)` - `fk_prices_user`: `user_id``users(id)`
### inventories
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| quantity | DECIMAL(18,3) | NOT NULL | 0.000 | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `product_id` - KEY: `idx_inventories_shop` (`shop_id`)
**Foreign Keys**: - `fk_inv_product`: `product_id``products(id)` ON DELETE CASCADE - `fk_inv_shop`: `shop_id``shops(id)` - `fk_inv_user`: `user_id``users(id)`
### customers
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(120) | NOT NULL | | |
| phone | VARCHAR(32) | YES | | |
| level | VARCHAR(32) | YES | | 客户等级标签 |
| price_level | ENUM('retail','distribution','wholesale','big_client') | NOT NULL | retail | 默认售价列 |
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_customers_shop` (`shop_id`) - KEY: `idx_customers_phone` (`phone`)
**Foreign Keys**: - `fk_customers_shop`: `shop_id``shops(id)` - `fk_customers_user`: `user_id``users(id)`
### suppliers
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(120) | NOT NULL | | |
| phone | VARCHAR(32) | YES | | |
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_suppliers_shop` (`shop_id`) - KEY: `idx_suppliers_phone` (`phone`)
**Foreign Keys**: - `fk_suppliers_shop`: `shop_id``shops(id)` - `fk_suppliers_user`: `user_id``users(id)`
### accounts
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(64) | NOT NULL | | |
| type | ENUM('cash','bank','alipay','wechat','other') | NOT NULL | cash | |
| balance | DECIMAL(18,2) | NOT NULL | 0.00 | |
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_accounts_shop` (`shop_id`) - UNIQUE: `ux_accounts_shop_name` (`shop_id`,`name`)
**Foreign Keys**: - `fk_accounts_shop`: `shop_id``shops(id)` - `fk_accounts_user`: `user_id``users(id)`
### sales_orders
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | 创建人 |
| customer_id | BIGINT UNSIGNED | YES | | |
| order_no | VARCHAR(32) | NOT NULL | | |
| order_time | DATETIME | NOT NULL | | |
| status | ENUM('draft','approved','returned','void') | NOT NULL | draft | |
| amount | DECIMAL(18,2) | NOT NULL | 0.00 | 应收合计 |
| paid_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 已收合计 |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sales_shop_time` (`shop_id`,`order_time`) - KEY: `idx_sales_customer` (`customer_id`) - UNIQUE: `ux_sales_order_no` (`shop_id`,`order_no`)
**Foreign Keys**: - `fk_sales_shop`: `shop_id``shops(id)` - `fk_sales_user`: `user_id``users(id)` - `fk_sales_customer`: `customer_id``customers(id)`
### sales_order_items
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| order_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| quantity | DECIMAL(18,3) | NOT NULL | | |
| unit_price | DECIMAL(18,2) | NOT NULL | | |
| discount_rate | DECIMAL(5,2) | NOT NULL | 0.00 | 折扣百分比0-100 |
| amount | DECIMAL(18,2) | NOT NULL | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_soi_order` (`order_id`) - KEY: `idx_soi_product` (`product_id`)
**Foreign Keys**: - `fk_soi_order`: `order_id``sales_orders(id)` ON DELETE CASCADE - `fk_soi_product`: `product_id``products(id)`
### purchase_orders
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| supplier_id | BIGINT UNSIGNED | YES | | |
| order_no | VARCHAR(32) | NOT NULL | | |
| order_time | DATETIME | NOT NULL | | |
| status | ENUM('draft','approved','void') | NOT NULL | draft | |
| amount | DECIMAL(18,2) | NOT NULL | 0.00 | 应付合计 |
| paid_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 已付合计 |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_purchase_shop_time` (`shop_id`,`order_time`) - KEY: `idx_purchase_supplier` (`supplier_id`) - UNIQUE: `ux_purchase_order_no` (`shop_id`,`order_no`)
**Foreign Keys**: - `fk_purchase_shop`: `shop_id``shops(id)` - `fk_purchase_user`: `user_id``users(id)` - `fk_purchase_supplier`: `supplier_id``suppliers(id)`
### purchase_order_items
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| order_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| quantity | DECIMAL(18,3) | NOT NULL | | |
| unit_price | DECIMAL(18,2) | NOT NULL | | |
| amount | DECIMAL(18,2) | NOT NULL | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_poi_order` (`order_id`) - KEY: `idx_poi_product` (`product_id`)
**Foreign Keys**: - `fk_poi_order`: `order_id``purchase_orders(id)` ON DELETE CASCADE - `fk_poi_product`: `product_id``products(id)`
### payments
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| biz_type | ENUM('sale','purchase','other') | NOT NULL | | |
| biz_id | BIGINT UNSIGNED | YES | | 业务表IDsales_orders/purchase_orders/other_transactions |
| account_id | BIGINT UNSIGNED | NOT NULL | | |
| direction | ENUM('in','out') | NOT NULL | | 收款/付款 |
| amount | DECIMAL(18,2) | NOT NULL | | |
| pay_time | DATETIME | NOT NULL | | |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_payments_shop_time` (`shop_id`,`pay_time`) - KEY: `idx_payments_biz` (`biz_type`,`biz_id`)
**Foreign Keys**: - `fk_payments_shop`: `shop_id``shops(id)` - `fk_payments_user`: `user_id``users(id)` - `fk_payments_account`: `account_id``accounts(id)`
### other_transactions
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| type | ENUM('income','expense') | NOT NULL | | |
| category | VARCHAR(64) | NOT NULL | | |
| counterparty_type | VARCHAR(32) | YES | | customer/supplier/other |
| counterparty_id | BIGINT UNSIGNED | YES | | |
| account_id | BIGINT UNSIGNED | NOT NULL | | |
| amount | DECIMAL(18,2) | NOT NULL | | |
| tx_time | DATETIME | NOT NULL | | |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ot_shop_time` (`shop_id`,`tx_time`) - KEY: `idx_ot_account` (`account_id`)
**Foreign Keys**: - `fk_ot_shop`: `shop_id``shops(id)` - `fk_ot_user`: `user_id``users(id)` - `fk_ot_account`: `account_id``accounts(id)`
### product_images 触发器
- `trg_products_ai`: AFTER INSERT ON `products` → 更新 `products.search_text`
- `trg_products_au`: BEFORE UPDATE ON `products` → 维护 `products.search_text`
- `trg_palias_ai`: AFTER INSERT ON `product_aliases` → 重建 `products.search_text`
- `trg_palias_au`: AFTER UPDATE ON `product_aliases` → 重建 `products.search_text`
- `trg_palias_ad`: AFTER DELETE ON `product_aliases` → 重建 `products.search_text`
### notices
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| title | VARCHAR(120) | NOT NULL | | |
| content | VARCHAR(500) | NOT NULL | | |
| tag | VARCHAR(32) | YES | | |
| is_pinned | TINYINT(1) | NOT NULL | 0 | |
| starts_at | DATETIME | YES | | |
| ends_at | DATETIME | YES | | |
| status | ENUM('draft','published','offline') | NOT NULL | published | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_notices_shop` (`shop_id`,`status`,`is_pinned`,`created_at`) - KEY: `idx_notices_time` (`starts_at`,`ends_at`)
**Foreign Keys**: - `fk_notices_shop`: `shop_id``shops(id)` ON UPDATE NO ACTION ON DELETE NO ACTION - `fk_notices_user`: `user_id``users(id)` ON UPDATE NO ACTION ON DELETE NO ACTION

67
doc/openapi.yaml Normal file
View File

@@ -0,0 +1,67 @@
openapi: 3.0.3
info:
title: PartsInquiry API
version: 0.1.0
description: >-
所有接口定义集中于此文件。每个 path 在 summary/description 中标注实现状态。
servers:
- url: /
paths:
/api/notices:
get:
summary: 公告列表(✅ Fully Implemented
description: 返回当前店铺可见的公告列表。后端与前端均已接入。
parameters:
- in: header
name: X-Shop-Id
required: false
schema:
type: integer
description: 店铺ID缺省为 1
responses:
'200':
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Notice'
components:
schemas:
Notice:
type: object
properties:
id:
type: integer
format: int64
shopId:
type: integer
format: int64
userId:
type: integer
format: int64
title:
type: string
content:
type: string
tag:
type: string
pinned:
type: boolean
startsAt:
type: string
format: date-time
endsAt:
type: string
format: date-time
status:
type: string
enum: [DRAFT, PUBLISHED, OFFLINE]
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time

14
frontend/common/config.js Normal file
View File

@@ -0,0 +1,14 @@
// 统一配置:禁止在业务代码中硬编码
// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值
const envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';
const storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';
const fallbackBaseUrl = 'http://localhost:8080';
export const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, '');
const envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';
const storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';
export const SHOP_ID = Number(envShopId || storageShopId || 1);

26
frontend/common/http.js Normal file
View File

@@ -0,0 +1,26 @@
import { API_BASE_URL, SHOP_ID } from './config.js'
function buildUrl(path) {
if (!path) return API_BASE_URL
if (path.startsWith('http')) return path
return API_BASE_URL + (path.startsWith('/') ? path : '/' + path)
}
export function get(path, params = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: buildUrl(path),
method: 'GET',
data: params,
header: { 'X-Shop-Id': SHOP_ID },
success: (res) => {
const { statusCode, data } = res
if (statusCode >= 200 && statusCode < 300) return resolve(data)
reject(new Error('HTTP ' + statusCode))
},
fail: (err) => reject(err)
})
})
}

View File

@@ -28,7 +28,10 @@
<!-- 公告栏自动轮播可点击查看详情 -->
<view class="notice">
<view class="notice-left">公告</view>
<swiper class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
<view v-if="loadingNotices" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">加载中...</view>
<view v-else-if="noticeError" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d;">{{ noticeError }}</view>
<view v-else-if="!notices.length" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">暂无公告</view>
<swiper v-else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
<swiper-item v-for="(n, idx) in notices" :key="idx">
<view class="notice-item" @click="onNoticeTap(n)">
<text class="notice-text">{{ n.text }}</text>
@@ -77,6 +80,7 @@
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return {
@@ -84,12 +88,9 @@
monthProfit: '0.00',
stockQty: '0.00',
activeTab: 'home',
notices: [
{ text: '选材精工:不锈钢、合金钢,耐腐蚀更耐用', tag: '品质' },
{ text: '表面工艺:电镀锌/镀镍/发黑处理,性能均衡', tag: '工艺' },
{ text: '库存齐全:螺丝、螺母、垫圈、膨胀螺栓等现货', tag: '库存' },
{ text: '企业采购支持:批量优惠,次日发货', tag: '服务' }
],
notices: [],
loadingNotices: false,
noticeError: '',
features: [
{ key: 'customer', title: '客户', img: '/static/icons/customer.png', emoji: '👥' },
{ key: 'sale', title: '销售', img: '/static/icons/sale.png', emoji: '💰' },
@@ -103,7 +104,25 @@
]
}
},
onLoad() {
this.fetchNotices()
},
methods: {
async fetchNotices() {
this.loadingNotices = true
this.noticeError = ''
try {
const list = await get('/api/notices')
this.notices = Array.isArray(list) ? list.map(n => ({
text: n.content || n.title || '',
tag: n.tag || ''
})) : []
} catch (e) {
this.noticeError = (e && e.message) || '公告加载失败'
} finally {
this.loadingNotices = false
}
},
onFeatureTap(item) {
uni.showToast({ title: item.title + '(开发中)', icon: 'none' })
},
@@ -113,7 +132,7 @@
onNoticeTap(n) {
uni.showModal({
title: '公告',
content: n.text,
content: n && (n.text || n.title || n.content) || '',
showCancel: false
})
},

View File

@@ -1 +1 @@
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\n","import App from './App'\n\n// #ifndef VUE3\nimport Vue from 'vue'\nimport './uni.promisify.adaptor'\nVue.config.productionTip = false\nApp.mpType = 'app'\nconst app = new Vue({\n ...App\n})\napp.$mount()\n// #endif\n\n// #ifdef VUE3\nimport { createSSRApp } from 'vue'\nexport function createApp() {\n const app = createSSRApp(App)\n return {\n app\n }\n}\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}

View File

@@ -1 +1 @@
{"version":3,"file":"assets.js","sources":["../../../static/metal-bg.jpg"],"sourcesContent":["export default \"/static/metal-bg.jpg\""],"names":[],"mappings":";AAAA,MAAe,aAAA;;"}
{"version":3,"file":"assets.js","sources":["../../../../../../static/metal-bg.jpg"],"sourcesContent":["export default \"/static/metal-bg.jpg\""],"names":[],"mappings":";AAAA,MAAe,aAAA;;"}

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\r\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\r\n\r\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\r\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\r\nconst fallbackBaseUrl = 'http://localhost:8080';\r\n\r\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\r\n\r\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\r\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\r\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\r\n\r\n\r\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAE/F,MAAM,YAAa,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AAC9H,MAAM,gBAAgB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,SAAS,KAAK,KAAM;AAC/E,MAAC,UAAU,OAAO,aAAa,iBAAiB,CAAC;;;"}

View File

@@ -0,0 +1 @@
{"version":3,"file":"http.js","sources":["common/http.js"],"sourcesContent":["import { API_BASE_URL, SHOP_ID } from './config.js'\r\n\r\nfunction buildUrl(path) {\r\n if (!path) return API_BASE_URL\r\n if (path.startsWith('http')) return path\r\n return API_BASE_URL + (path.startsWith('/') ? path : '/' + path)\r\n}\r\n\r\nexport function get(path, params = {}) {\r\n return new Promise((resolve, reject) => {\r\n uni.request({\r\n url: buildUrl(path),\r\n method: 'GET',\r\n data: params,\r\n header: { 'X-Shop-Id': SHOP_ID },\r\n success: (res) => {\r\n const { statusCode, data } = res\r\n if (statusCode >= 200 && statusCode < 300) return resolve(data)\r\n reject(new Error('HTTP ' + statusCode))\r\n },\r\n fail: (err) => reject(err)\r\n })\r\n })\r\n}\r\n\r\n\r\n"],"names":["API_BASE_URL","uni","SHOP_ID"],"mappings":";;;AAEA,SAAS,SAAS,MAAM;AACtB,MAAI,CAAC;AAAM,WAAOA,cAAY;AAC9B,MAAI,KAAK,WAAW,MAAM;AAAG,WAAO;AACpC,SAAOA,cAAAA,gBAAgB,KAAK,WAAW,GAAG,IAAI,OAAO,MAAM;AAC7D;AAEO,SAAS,IAAI,MAAM,SAAS,IAAI;AACrC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtCC,kBAAAA,MAAI,QAAQ;AAAA,MACV,KAAK,SAAS,IAAI;AAAA,MAClB,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,QAAQ,EAAE,aAAaC,sBAAS;AAAA,MAChC,SAAS,CAAC,QAAQ;AAChB,cAAM,EAAE,YAAY,KAAI,IAAK;AAC7B,YAAI,cAAc,OAAO,aAAa;AAAK,iBAAO,QAAQ,IAAI;AAC9D,eAAO,IAAI,MAAM,UAAU,UAAU,CAAC;AAAA,MACvC;AAAA,MACD,MAAM,CAAC,QAAQ,OAAO,GAAG;AAAA,IAC/B,CAAK;AAAA,EACL,CAAG;AACH;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
"use strict";
const common_vendor = require("./vendor.js");
const envBaseUrl = typeof process !== "undefined" && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL) || "";
const storageBaseUrl = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("API_BASE_URL") || "" : "";
const fallbackBaseUrl = "http://localhost:8080";
const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, "");
const envShopId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID) || "";
const storageShopId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("SHOP_ID") || "" : "";
const SHOP_ID = Number(envShopId || storageShopId || 1);
exports.API_BASE_URL = API_BASE_URL;
exports.SHOP_ID = SHOP_ID;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/config.js.map

View File

@@ -0,0 +1,29 @@
"use strict";
const common_vendor = require("./vendor.js");
const common_config = require("./config.js");
function buildUrl(path) {
if (!path)
return common_config.API_BASE_URL;
if (path.startsWith("http"))
return path;
return common_config.API_BASE_URL + (path.startsWith("/") ? path : "/" + path);
}
function get(path, params = {}) {
return new Promise((resolve, reject) => {
common_vendor.index.request({
url: buildUrl(path),
method: "GET",
data: params,
header: { "X-Shop-Id": common_config.SHOP_ID },
success: (res) => {
const { statusCode, data } = res;
if (statusCode >= 200 && statusCode < 300)
return resolve(data);
reject(new Error("HTTP " + statusCode));
},
fail: (err) => reject(err)
});
});
}
exports.get = get;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/http.js.map

View File

@@ -6917,9 +6917,9 @@ function isConsoleWritable() {
return isWritable;
}
function initRuntimeSocketService() {
const hosts = "198.18.0.1,192.168.31.107,127.0.0.1";
const hosts = "198.18.0.1,192.168.31.193,127.0.0.1";
const port = "8090";
const id = "mp-weixin_BJ7qAd";
const id = "mp-weixin_UjOtWQ";
const lazy = typeof swan !== "undefined";
let restoreError = lazy ? () => {
} : initOnError();

View File

@@ -1,5 +1,6 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const common_assets = require("../../common/assets.js");
const _sfc_main = {
data() {
@@ -8,12 +9,9 @@ const _sfc_main = {
monthProfit: "0.00",
stockQty: "0.00",
activeTab: "home",
notices: [
{ text: "选材精工:不锈钢、合金钢,耐腐蚀更耐用", tag: "品质" },
{ text: "表面工艺:电镀锌/镀镍/发黑处理,性能均衡", tag: "工艺" },
{ text: "库存齐全:螺丝、螺母、垫圈、膨胀螺栓等现货", tag: "库存" },
{ text: "企业采购支持:批量优惠,次日发货", tag: "服务" }
],
notices: [],
loadingNotices: false,
noticeError: "",
features: [
{ key: "customer", title: "客户", img: "/static/icons/customer.png", emoji: "👥" },
{ key: "sale", title: "销售", img: "/static/icons/sale.png", emoji: "💰" },
@@ -27,7 +25,25 @@ const _sfc_main = {
]
};
},
onLoad() {
this.fetchNotices();
},
methods: {
async fetchNotices() {
this.loadingNotices = true;
this.noticeError = "";
try {
const list = await common_http.get("/api/notices");
this.notices = Array.isArray(list) ? list.map((n) => ({
text: n.content || n.title || "",
tag: n.tag || ""
})) : [];
} catch (e) {
this.noticeError = e && e.message || "公告加载失败";
} finally {
this.loadingNotices = false;
}
},
onFeatureTap(item) {
common_vendor.index.showToast({ title: item.title + "(开发中)", icon: "none" });
},
@@ -37,7 +53,7 @@ const _sfc_main = {
onNoticeTap(n) {
common_vendor.index.showModal({
title: "公告",
content: n.text,
content: n && (n.text || n.title || n.content) || "",
showCancel: false
});
},
@@ -50,12 +66,16 @@ const _sfc_main = {
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
return common_vendor.e({
a: common_assets._imports_0,
b: common_vendor.t($data.todayAmount),
c: common_vendor.t($data.monthProfit),
d: common_vendor.t($data.stockQty),
e: common_vendor.f($data.notices, (n, idx, i0) => {
e: $data.loadingNotices
}, $data.loadingNotices ? {} : $data.noticeError ? {
g: common_vendor.t($data.noticeError)
} : !$data.notices.length ? {} : {
i: common_vendor.f($data.notices, (n, idx, i0) => {
return common_vendor.e({
a: common_vendor.t(n.text),
b: n.tag
@@ -65,9 +85,12 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
d: common_vendor.o(($event) => $options.onNoticeTap(n), idx),
e: idx
});
}),
f: common_vendor.o((...args) => $options.onNoticeList && $options.onNoticeList(...args)),
g: common_vendor.f($data.features, (item, k0, i0) => {
})
}, {
f: $data.noticeError,
h: !$data.notices.length,
j: common_vendor.o((...args) => $options.onNoticeList && $options.onNoticeList(...args)),
k: common_vendor.f($data.features, (item, k0, i0) => {
return common_vendor.e({
a: item.img
}, item.img ? {
@@ -82,14 +105,14 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
h: common_vendor.o(($event) => $options.onFeatureTap(item), item.key)
});
}),
h: $data.activeTab === "home" ? 1 : "",
i: common_vendor.o(($event) => $data.activeTab = "home"),
j: common_vendor.o((...args) => $options.onCreateOrder && $options.onCreateOrder(...args)),
k: $data.activeTab === "detail" ? 1 : "",
l: common_vendor.o(($event) => $data.activeTab = "detail"),
m: $data.activeTab === "me" ? 1 : "",
n: common_vendor.o(($event) => $data.activeTab = "me")
};
l: $data.activeTab === "home" ? 1 : "",
m: common_vendor.o(($event) => $data.activeTab = "home"),
n: common_vendor.o((...args) => $options.onCreateOrder && $options.onCreateOrder(...args)),
o: $data.activeTab === "detail" ? 1 : "",
p: common_vendor.o(($event) => $data.activeTab = "detail"),
q: $data.activeTab === "me" ? 1 : "",
r: common_vendor.o(($event) => $data.activeTab = "me")
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);

View File

@@ -1 +1 @@
<view class="home"><image class="home-bg" src="{{a}}" mode="aspectFill"></image><view class="hero"><view class="hero-top"><text class="brand">五金配件管家</text><view class="cta"><text class="cta-text">咨询</text></view></view><view class="kpi"><view class="kpi-item"><text class="kpi-label">今日销售额</text><text class="kpi-value">{{b}}</text></view><view class="kpi-item"><text class="kpi-label">本月利润</text><text class="kpi-value">{{c}}</text></view><view class="kpi-item"><text class="kpi-label">库存数量</text><text class="kpi-value">{{d}}</text></view></view></view><view class="notice"><view class="notice-left">公告</view><swiper class="notice-swiper" circular autoplay interval="4000" duration="400" vertical><swiper-item wx:for="{{e}}" wx:for-item="n" wx:key="e"><view class="notice-item" bindtap="{{n.d}}"><text class="notice-text">{{n.a}}</text><text wx:if="{{n.b}}" class="notice-tag">{{n.c}}</text></view></swiper-item></swiper><view class="notice-right" bindtap="{{f}}">更多</view></view><view class="section-title"><text class="section-text">常用功能</text></view><view class="grid-wrap"><view class="grid"><view wx:for="{{g}}" wx:for-item="item" wx:key="g" class="grid-item" bindtap="{{item.h}}"><view class="icon icon-squircle"><image wx:if="{{item.a}}" src="{{item.b}}" class="icon-img" mode="aspectFit" binderror="{{item.c}}"></image><text wx:elif="{{item.d}}" class="icon-emoji">{{item.e}}</text><view wx:else class="icon-placeholder"></view></view><text class="grid-chip">{{item.f}}</text></view></view></view><view class="bottom-bar"><view class="{{['tab', h && 'active']}}" bindtap="{{i}}"><text>首页</text></view><view class="tab primary" bindtap="{{j}}"><text>开单</text></view><view class="{{['tab', k && 'active']}}" bindtap="{{l}}"><text>明细</text></view><view class="{{['tab', m && 'active']}}" bindtap="{{n}}"><text>我的</text></view></view></view>
<view class="home"><image class="home-bg" src="{{a}}" mode="aspectFill"></image><view class="hero"><view class="hero-top"><text class="brand">五金配件管家</text><view class="cta"><text class="cta-text">咨询</text></view></view><view class="kpi"><view class="kpi-item"><text class="kpi-label">今日销售额</text><text class="kpi-value">{{b}}</text></view><view class="kpi-item"><text class="kpi-label">本月利润</text><text class="kpi-value">{{c}}</text></view><view class="kpi-item"><text class="kpi-label">库存数量</text><text class="kpi-value">{{d}}</text></view></view></view><view class="notice"><view class="notice-left">公告</view><view wx:if="{{e}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">加载中...</view><view wx:elif="{{f}}" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d">{{g}}</view><view wx:elif="{{h}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">暂无公告</view><swiper wx:else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical><swiper-item wx:for="{{i}}" wx:for-item="n" wx:key="e"><view class="notice-item" bindtap="{{n.d}}"><text class="notice-text">{{n.a}}</text><text wx:if="{{n.b}}" class="notice-tag">{{n.c}}</text></view></swiper-item></swiper><view class="notice-right" bindtap="{{j}}">更多</view></view><view class="section-title"><text class="section-text">常用功能</text></view><view class="grid-wrap"><view class="grid"><view wx:for="{{k}}" wx:for-item="item" wx:key="g" class="grid-item" bindtap="{{item.h}}"><view class="icon icon-squircle"><image wx:if="{{item.a}}" src="{{item.b}}" class="icon-img" mode="aspectFit" binderror="{{item.c}}"></image><text wx:elif="{{item.d}}" class="icon-emoji">{{item.e}}</text><view wx:else class="icon-placeholder"></view></view><text class="grid-chip">{{item.f}}</text></view></view></view><view class="bottom-bar"><view class="{{['tab', l && 'active']}}" bindtap="{{m}}"><text>首页</text></view><view class="tab primary" bindtap="{{n}}"><text>开单</text></view><view class="{{['tab', o && 'active']}}" bindtap="{{p}}"><text>明细</text></view><view class="{{['tab', q && 'active']}}" bindtap="{{r}}"><text>我的</text></view></view></view>