1.0
This commit is contained in:
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/awesomeProject.iml" filepath="$PROJECT_DIR$/.idea/awesomeProject.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Data Transfer
|
||||||
|
|
||||||
|
Source Server : local
|
||||||
|
Source Server Type : MySQL
|
||||||
|
Source Server Version : 80041 (8.0.41)
|
||||||
|
Source Host : localhost:3306
|
||||||
|
Source Schema : a_business_help
|
||||||
|
|
||||||
|
Target Server Type : MySQL
|
||||||
|
Target Server Version : 80041 (8.0.41)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 27/12/2025 17:18:06
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for banners
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `banners`;
|
||||||
|
CREATE TABLE `banners` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '轮播图标题',
|
||||||
|
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '轮播图图片URL',
|
||||||
|
`link_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '点击跳转链接',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '描述信息',
|
||||||
|
`sort_order` bigint NOT NULL DEFAULT 0 COMMENT '排序序号',
|
||||||
|
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0禁用,1启用',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of banners
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `banners` VALUES (1, '1', '/back/file/20250916110937_f2a179bd.png', '', '', 0, 1, '2025-09-16 11:09:41', '2025-09-16 11:09:41');
|
||||||
|
INSERT INTO `banners` VALUES (2, '2', '/back/file/20250916111002_09ee01c2.png', '', '', 1, 1, '2025-09-16 11:10:03', '2025-09-16 11:11:24');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for config_changes
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `config_changes`;
|
||||||
|
CREATE TABLE `config_changes` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`config_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置键',
|
||||||
|
`old_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '旧值',
|
||||||
|
`new_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '新值',
|
||||||
|
`change_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '变更类型',
|
||||||
|
`changed_by` bigint NOT NULL COMMENT '修改人ID',
|
||||||
|
`changed_at` bigint NOT NULL COMMENT '修改时间戳',
|
||||||
|
`extra` json NULL COMMENT '额外信息',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of config_changes
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for order_messages
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `order_messages`;
|
||||||
|
CREATE TABLE `order_messages` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_id` bigint UNSIGNED NOT NULL,
|
||||||
|
`order_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'purchase',
|
||||||
|
`user_id` bigint UNSIGNED NOT NULL,
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`created_at` datetime(3) NULL DEFAULT NULL,
|
||||||
|
`updated_at` datetime(3) NULL DEFAULT NULL,
|
||||||
|
`images` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `idx_order_messages_order_id`(`order_id` ASC) USING BTREE,
|
||||||
|
INDEX `idx_order_messages_user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_order_messages_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of order_messages
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `order_messages` VALUES (1, 13, 'purchase', 8, '你好', '2025-09-13 14:04:51.529', '2025-09-13 14:04:51.529', NULL);
|
||||||
|
INSERT INTO `order_messages` VALUES (2, 6, 'sales', 8, '已完成', '2025-09-13 14:07:19.470', '2025-09-13 14:07:19.470', NULL);
|
||||||
|
INSERT INTO `order_messages` VALUES (3, 17, 'purchase', 7, '尽快确认', '2025-09-13 14:22:25.648', '2025-09-13 14:22:25.648', NULL);
|
||||||
|
INSERT INTO `order_messages` VALUES (4, 7, 'sales', 8, '了解', '2025-09-13 15:19:49.892', '2025-09-13 15:19:49.892', NULL);
|
||||||
|
INSERT INTO `order_messages` VALUES (5, 21, 'purchase', 1, '完成', '2025-09-13 16:48:34.984', '2025-09-13 16:48:34.984', NULL);
|
||||||
|
INSERT INTO `order_messages` VALUES (6, 7, 'purchase', 7, '凭证在此', '2025-09-16 12:31:32.666', '2025-09-16 12:31:32.666', '/back/file/20250916123132_da65b168.png');
|
||||||
|
INSERT INTO `order_messages` VALUES (7, 22, 'purchase', 7, '你好', '2025-09-16 12:36:53.652', '2025-09-16 12:36:53.652', '');
|
||||||
|
INSERT INTO `order_messages` VALUES (8, 22, 'purchase', 7, '阿斯顿', '2025-09-16 12:43:38.617', '2025-09-16 12:43:38.617', '/back/file/20250916124338_dededd0c.png');
|
||||||
|
INSERT INTO `order_messages` VALUES (9, 9, 'sales', 7, '阿萨', '2025-09-16 12:53:47.556', '2025-09-16 12:53:47.556', '/back/file/20250916125347_dededd0c.png');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for primary_products
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `primary_products`;
|
||||||
|
CREATE TABLE `primary_products` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`category_id` bigint UNSIGNED NOT NULL COMMENT '分类ID',
|
||||||
|
`product_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称',
|
||||||
|
`product_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品描述',
|
||||||
|
`product_images` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品图片URLs,逗号分割',
|
||||||
|
`original_price` decimal(10, 2) NOT NULL COMMENT '原价',
|
||||||
|
`current_price` decimal(10, 2) NOT NULL COMMENT '现价',
|
||||||
|
`stock_quantity` bigint NOT NULL DEFAULT 0 COMMENT '库存数量',
|
||||||
|
`sales_count` bigint NOT NULL DEFAULT 0 COMMENT '销量',
|
||||||
|
`view_count` bigint NOT NULL DEFAULT 0 COMMENT '浏览量',
|
||||||
|
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0下架,1上架',
|
||||||
|
`sort_order` bigint NOT NULL DEFAULT 0 COMMENT '排序序号',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`is_hot` tinyint NOT NULL DEFAULT 0 COMMENT '是否为热门商品:0否,1是',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `fk_primary_products_category`(`category_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_primary_products_category` FOREIGN KEY (`category_id`) REFERENCES `product_categories` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of primary_products
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `primary_products` VALUES (1, 4, '酒水', '酒水酒水', '/back/file/20250902104133_a0ac0a17.jpeg,/back/file/20250902104136_e18fe0eb.jpeg', 999.00, 888.00, 9976, 29, 32, 1, 0, '2025-09-02 10:41:47', '2025-12-20 16:02:04', 1);
|
||||||
|
INSERT INTO `primary_products` VALUES (2, 3, '测试', '啊手动阀手动阀', '/back/file/20250916115526_fc06f62c.png', 99.00, 77.00, 72, 5, 4, 1, 0, '2025-09-16 11:55:34', '2025-12-20 15:30:16', 0);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_categories
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_categories`;
|
||||||
|
CREATE TABLE `product_categories` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`category_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分类名称',
|
||||||
|
`category_icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '分类图标URL',
|
||||||
|
`sort_order` bigint NOT NULL DEFAULT 0 COMMENT '排序序号',
|
||||||
|
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0禁用,1启用',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of product_categories
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `product_categories` VALUES (1, '学习培训', '/back/file/20250902091924_9b4298ba.png', 0, 1, '2025-09-02 09:19:27', '2025-09-02 10:47:09');
|
||||||
|
INSERT INTO `product_categories` VALUES (2, '美食', '/back/file/20250902092108_e056556d.png', 1, 1, '2025-09-02 09:21:20', '2025-09-02 09:21:20');
|
||||||
|
INSERT INTO `product_categories` VALUES (3, '酒店', '/back/file/20250902102539_639669b0.png', 0, 1, '2025-09-02 10:25:40', '2025-09-02 10:25:40');
|
||||||
|
INSERT INTO `product_categories` VALUES (4, '酒水饮料', '/back/file/20250902102604_883b4677.png', 3, 1, '2025-09-02 10:26:08', '2025-09-02 10:26:08');
|
||||||
|
INSERT INTO `product_categories` VALUES (5, 'yi', '/back/file/20250916105653_0b6dc315.png', 0, 1, '2025-09-16 10:57:00', '2025-09-16 10:57:00');
|
||||||
|
INSERT INTO `product_categories` VALUES (6, '二', '/back/file/20250916105705_0b6dc315.png', 0, 1, '2025-09-16 10:57:08', '2025-09-16 10:57:08');
|
||||||
|
INSERT INTO `product_categories` VALUES (7, '三', '/back/file/20250916105715_0b6dc315.png', 0, 1, '2025-09-16 10:57:15', '2025-09-16 10:57:15');
|
||||||
|
INSERT INTO `product_categories` VALUES (8, '四', '/back/file/20250916105722_0b6dc315.png', 0, 1, '2025-09-16 10:57:23', '2025-09-16 10:57:23');
|
||||||
|
INSERT INTO `product_categories` VALUES (9, '五', '/back/file/20250916105729_0b6dc315.png', 0, 1, '2025-09-16 10:57:30', '2025-09-16 10:57:30');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for purchase_orders
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `purchase_orders`;
|
||||||
|
CREATE TABLE `purchase_orders` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '订单编号',
|
||||||
|
`buyer_id` bigint UNSIGNED NOT NULL COMMENT '买家用户ID',
|
||||||
|
`seller_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '卖家用户ID(二级商品交易时使用)',
|
||||||
|
`product_id` bigint NOT NULL COMMENT '商品ID',
|
||||||
|
`product_type` tinyint NOT NULL COMMENT '商品类型:1一级商品,2二级商品',
|
||||||
|
`product_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称',
|
||||||
|
`product_images` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品图片URLs,逗号分割',
|
||||||
|
`unit_price` decimal(10, 2) NOT NULL COMMENT '单价',
|
||||||
|
`quantity` bigint NOT NULL DEFAULT 1 COMMENT '数量',
|
||||||
|
`total_amount` decimal(10, 2) NOT NULL COMMENT '总金额',
|
||||||
|
`address_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '收货地址ID',
|
||||||
|
`order_status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1待确认,2已完成,3已取消',
|
||||||
|
`confirm_time` timestamp NULL DEFAULT NULL COMMENT '确认时间',
|
||||||
|
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '备注',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`payment_proof_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `idx_purchase_orders_order_no`(`order_no` ASC) USING BTREE,
|
||||||
|
INDEX `fk_purchase_orders_buyer`(`buyer_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_purchase_orders_seller`(`seller_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_purchase_orders_address`(`address_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_purchase_orders_address` FOREIGN KEY (`address_id`) REFERENCES `user_addresses` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_purchase_orders_buyer` FOREIGN KEY (`buyer_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_purchase_orders_seller` FOREIGN KEY (`seller_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of purchase_orders
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `purchase_orders` VALUES (1, 'PO17568750558', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 2, 1776.00, 1, 3, NULL, '', '2025-09-03 12:50:55', '2025-09-03 12:57:31', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (2, 'PO17568755868', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 2, 1776.00, 1, 2, '2025-09-03 13:25:21', '完成了', '2025-09-03 12:59:46', '2025-09-03 13:25:21', '/back/file/20250903125954_d771c101.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (3, 'PO17568780478', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 2, '2025-09-03 13:41:10', '欧克', '2025-09-03 13:40:47', '2025-09-03 13:41:10', '/back/file/20250903134051_ea22e0d9.jpeg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (4, 'PO17568782318', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 2, '2025-09-03 13:44:35', '欧克', '2025-09-03 13:43:51', '2025-09-03 13:44:35', '/back/file/20250903134356_d771c101.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (5, 'PO17568784918', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 2, '2025-09-03 13:48:41', '完成', '2025-09-03 13:48:11', '2025-09-03 13:48:41', '/back/file/20250903134815_d771c101.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (6, 'PO17568801536', 6, 8, 1, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 899.00, 1, 899.00, 2, 3, NULL, '', '2025-09-03 14:15:53', '2025-09-03 17:12:32', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (7, 'PO17568835147', 7, 8, 2, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 3, 2, '2025-09-03 15:43:47', '', '2025-09-03 15:11:54', '2025-09-03 15:43:47', '/back/file/20250903153218_ea22e0d9.jpeg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (8, 'PO17569507758', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 3, NULL, '', '2025-09-04 09:52:55', '2025-09-04 09:53:11', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (9, 'PO17569509006', 6, 8, 5, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 2, 3, NULL, '', '2025-09-04 09:55:00', '2025-09-04 09:55:18', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (10, 'PO17569509266', 6, 8, 5, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 2, 3, NULL, '', '2025-09-04 09:55:26', '2025-09-04 10:27:58', '/back/file/20250904101417_7c6bf467.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (11, 'PO17577382648', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 3, NULL, '', '2025-09-13 12:37:44', '2025-09-13 13:15:00', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (12, 'PO17577385698', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 3, NULL, '', '2025-09-13 12:42:49', '2025-09-13 13:15:00', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (13, 'PO17577389748', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 1, 2, '2025-09-13 12:52:02', '啊', '2025-09-13 12:49:34', '2025-09-13 12:52:02', '/back/file/20250913125113_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (14, 'PO17577391807', 7, 8, 6, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 2, '2025-09-13 12:56:18', '', '2025-09-13 12:53:00', '2025-09-13 12:56:18', '/back/file/20250913125535_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (15, 'PO17577392837', 7, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 3, 3, NULL, '', '2025-09-13 12:54:43', '2025-09-13 13:30:00', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (16, 'PO17577394497', 7, 8, 5, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 1, NULL, '', '2025-09-13 12:57:29', '2025-09-13 12:57:34', '/back/file/20250913125733_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (17, 'PO17577445297', 7, 8, 7, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 1, NULL, '', '2025-09-13 14:22:09', '2025-09-13 14:22:15', '/back/file/20250913142213_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (18, 'PO17577518998', 8, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 5, 4440.00, 1, 1, NULL, '', '2025-09-13 16:24:59', '2025-09-13 16:25:03', '/back/file/20250913162502_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (19, 'PO17577522668', 8, 7, 8, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 870.00, 1, 870.00, 1, 2, '2025-09-13 16:31:35', '', '2025-09-13 16:31:06', '2025-09-13 16:31:35', '/back/file/20250913163110_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (20, 'PO17577524237', 7, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 3, 2, '2025-09-13 16:34:03', '啊', '2025-09-13 16:33:43', '2025-09-13 16:34:03', '/back/file/20250913163347_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (21, 'PO17577524978', 8, 7, 9, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 1, 2, '2025-09-13 16:35:27', '', '2025-09-13 16:34:57', '2025-09-13 16:35:27', '/back/file/20250913163500_3657ddb6.jpg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (22, 'PO17579957867', 7, NULL, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 1, 77.00, 3, 2, '2025-09-16 12:10:09', '完成', '2025-09-16 12:09:46', '2025-09-16 12:10:09', '/back/file/20250916120952_dededd0c.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (23, 'PO17586830156', 6, 7, 10, 2, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 1, 68.00, 2, 2, '2025-09-24 11:05:08', '', '2025-09-24 11:03:35', '2025-09-24 11:05:08', '/back/file/20250924110357_ea22e0d9.jpeg');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (24, 'PO17586847026', 6, 8, 4, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 2, 1, NULL, '', '2025-09-24 11:31:42', '2025-09-24 11:32:21', '/back/file/20250924113219_0b6dc315.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (25, 'PO17586847646', 6, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 10, 8880.00, 2, 2, '2025-09-24 11:33:59', '而无法', '2025-09-24 11:32:44', '2025-09-24 11:33:59', '/back/file/20250924113247_883b4677.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (26, 'PO17586849757', 7, 6, 11, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 2, '2025-09-24 11:45:30', '', '2025-09-24 11:36:15', '2025-09-24 11:45:30', '/back/file/20250924113618_883b4677.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (27, 'PO17662156171', 1, NULL, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 1, 77.00, 5, 2, '2025-12-20 15:29:00', 'wc1', '2025-12-20 15:26:57', '2025-12-20 15:29:00', '/back/file/20251220152707_b89ec398.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (28, 'PO17662157691', 1, NULL, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 1, 77.00, 5, 2, '2025-12-20 15:30:09', '123', '2025-12-20 15:29:29', '2025-12-20 15:30:09', '/back/file/20251220152938_71cb232a.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (29, 'PO17662157911', 1, NULL, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 1, 77.00, 5, 2, '2025-12-20 15:30:06', ' 123', '2025-12-20 15:29:51', '2025-12-20 15:30:06', '/back/file/20251220152955_2e30c931.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (30, 'PO17662158151', 1, NULL, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 1, 77.00, 5, 1, NULL, '', '2025-12-20 15:30:15', '2025-12-20 15:30:20', '/back/file/20251220153019_2e30c931.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (31, 'PO17662160932', 2, 1, 14, 2, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 1, 68.00, 6, 2, '2025-12-20 16:01:52', '', '2025-12-20 15:34:53', '2025-12-20 16:01:52', '/back/file/20251220153458_02f6f844.png');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (32, 'PO17662177231', 1, NULL, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 5, 0, NULL, '', '2025-12-20 16:02:03', '2025-12-20 16:02:03', '');
|
||||||
|
INSERT INTO `purchase_orders` VALUES (33, 'PO17662179888', 8, 1, 13, 2, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 1, 68.00, 4, 0, NULL, '', '2025-12-20 16:06:28', '2025-12-20 16:06:28', '');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for sales_orders
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `sales_orders`;
|
||||||
|
CREATE TABLE `sales_orders` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '订单编号',
|
||||||
|
`seller_id` bigint UNSIGNED NOT NULL COMMENT '卖家用户ID',
|
||||||
|
`buyer_id` bigint UNSIGNED NOT NULL COMMENT '买家用户ID',
|
||||||
|
`secondary_product_id` bigint UNSIGNED NOT NULL COMMENT '二级商品ID',
|
||||||
|
`product_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称',
|
||||||
|
`product_images` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品图片URLs,逗号分割',
|
||||||
|
`selling_price` decimal(10, 2) NOT NULL COMMENT '出售价格',
|
||||||
|
`quantity` bigint NOT NULL DEFAULT 1 COMMENT '数量',
|
||||||
|
`total_amount` decimal(10, 2) NOT NULL COMMENT '总金额',
|
||||||
|
`address_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '收货地址ID',
|
||||||
|
`order_status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1待发货,2确认完成,3已取消',
|
||||||
|
`complete_time` timestamp NULL DEFAULT NULL COMMENT '完成时间',
|
||||||
|
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '备注',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`payment_proof_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`purchase_order_id` bigint UNSIGNED NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `idx_sales_orders_order_no`(`order_no` ASC) USING BTREE,
|
||||||
|
INDEX `fk_sales_orders_secondary_product`(`secondary_product_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_sales_orders_address`(`address_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_sales_orders_seller`(`seller_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_sales_orders_buyer`(`buyer_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_sales_orders_purchase_order`(`purchase_order_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_sales_orders_address` FOREIGN KEY (`address_id`) REFERENCES `user_addresses` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_sales_orders_buyer` FOREIGN KEY (`buyer_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_sales_orders_purchase_order` FOREIGN KEY (`purchase_order_id`) REFERENCES `purchase_orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_sales_orders_secondary_product` FOREIGN KEY (`secondary_product_id`) REFERENCES `secondary_products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_sales_orders_seller` FOREIGN KEY (`seller_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of sales_orders
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `sales_orders` VALUES (1, 'SO17568801538', 8, 6, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 899.00, 1, 899.00, 2, 3, NULL, '', '2025-09-03 14:15:53', '2025-12-20 16:04:53', '', 6);
|
||||||
|
INSERT INTO `sales_orders` VALUES (2, 'SO17568835148', 8, 7, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 1, 888.00, 3, 2, '2025-09-03 15:43:47', '', '2025-09-03 15:11:54', '2025-09-03 15:43:47', '/back/file/20250903153218_ea22e0d9.jpeg', 7);
|
||||||
|
INSERT INTO `sales_orders` VALUES (3, 'SO17569509008', 8, 6, 5, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 2, 3, NULL, '', '2025-09-04 09:55:00', '2025-12-20 16:04:53', '', 9);
|
||||||
|
INSERT INTO `sales_orders` VALUES (4, 'SO17569509268', 8, 6, 5, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 2, 3, NULL, '不完整', '2025-09-04 09:55:26', '2025-12-20 16:04:53', '/back/file/20250904101417_7c6bf467.png', 10);
|
||||||
|
INSERT INTO `sales_orders` VALUES (5, 'SO17577391808', 8, 7, 6, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 2, '2025-09-13 12:56:18', '', '2025-09-13 12:53:00', '2025-09-13 12:56:18', '/back/file/20250913125535_3657ddb6.jpg', 14);
|
||||||
|
INSERT INTO `sales_orders` VALUES (6, 'SO17577394498', 8, 7, 5, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 2, '2025-09-13 12:57:54', '完成', '2025-09-13 12:57:29', '2025-09-13 12:57:54', '/back/file/20250913125733_3657ddb6.jpg', 16);
|
||||||
|
INSERT INTO `sales_orders` VALUES (7, 'SO17577445298', 8, 7, 7, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 2, '2025-09-13 16:26:39', '完成', '2025-09-13 14:22:09', '2025-09-13 16:26:39', '/back/file/20250913142213_3657ddb6.jpg', 17);
|
||||||
|
INSERT INTO `sales_orders` VALUES (8, 'SO17577522667', 7, 8, 8, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 870.00, 1, 870.00, 1, 2, '2025-09-13 16:31:35', '', '2025-09-13 16:31:06', '2025-09-13 16:31:35', '/back/file/20250913163110_3657ddb6.jpg', 19);
|
||||||
|
INSERT INTO `sales_orders` VALUES (9, 'SO17577524977', 7, 8, 9, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 1, 2, '2025-09-13 16:35:27', '', '2025-09-13 16:34:57', '2025-09-13 16:35:27', '/back/file/20250913163500_3657ddb6.jpg', 21);
|
||||||
|
INSERT INTO `sales_orders` VALUES (10, 'SO17586830157', 7, 6, 10, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 1, 68.00, 2, 2, '2025-09-24 11:05:08', '', '2025-09-24 11:03:35', '2025-09-24 11:05:08', '/back/file/20250924110357_ea22e0d9.jpeg', 23);
|
||||||
|
INSERT INTO `sales_orders` VALUES (11, 'SO17586847028', 8, 6, 4, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 2, 1, NULL, '', '2025-09-24 11:31:42', '2025-09-24 11:32:21', '/back/file/20250924113219_0b6dc315.png', 24);
|
||||||
|
INSERT INTO `sales_orders` VALUES (12, 'SO17586849756', 6, 7, 11, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 1, 879.00, 3, 2, '2025-09-24 11:45:30', '', '2025-09-24 11:36:15', '2025-09-24 11:45:30', '/back/file/20250924113618_883b4677.png', 26);
|
||||||
|
INSERT INTO `sales_orders` VALUES (13, 'SO17662160931', 1, 2, 14, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 1, 68.00, 6, 2, '2025-12-20 16:01:52', '', '2025-12-20 15:34:53', '2025-12-20 16:01:52', '/back/file/20251220153458_02f6f844.png', 31);
|
||||||
|
INSERT INTO `sales_orders` VALUES (14, 'SO17662179881', 1, 8, 13, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 1, 68.00, 4, 0, NULL, '', '2025-12-20 16:06:28', '2025-12-20 16:06:28', '', 33);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for score_records
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `score_records`;
|
||||||
|
CREATE TABLE `score_records` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint UNSIGNED NOT NULL COMMENT '用户编号',
|
||||||
|
`change_number` bigint NOT NULL COMMENT '变化数量',
|
||||||
|
`note` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '说明',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `fk_score_records_user`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_score_records_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of score_records
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `score_records` VALUES (1, 4, 5, 'ig', '2025-09-02 09:00:33', '2025-09-02 09:00:33');
|
||||||
|
INSERT INTO `score_records` VALUES (3, 5, 5, '赠送', '2025-09-03 16:32:08', '2025-09-03 16:32:08');
|
||||||
|
INSERT INTO `score_records` VALUES (4, 8, 3000, '系统充值', '2025-09-03 16:55:00', '2025-09-03 16:55:00');
|
||||||
|
INSERT INTO `score_records` VALUES (5, 8, -500, '扣除', '2025-09-03 16:55:14', '2025-09-03 16:55:14');
|
||||||
|
INSERT INTO `score_records` VALUES (6, 8, 9, '系统策略', '2025-09-13 12:56:18', '2025-09-13 12:56:18');
|
||||||
|
INSERT INTO `score_records` VALUES (7, 7, 9, '系统策略', '2025-09-13 16:31:34', '2025-09-13 16:31:34');
|
||||||
|
INSERT INTO `score_records` VALUES (8, 7, 9, '系统策略', '2025-09-13 16:35:27', '2025-09-13 16:35:27');
|
||||||
|
INSERT INTO `score_records` VALUES (9, 7, 9, '系统策略', '2025-09-24 11:05:07', '2025-09-24 11:05:07');
|
||||||
|
INSERT INTO `score_records` VALUES (10, 6, 9, '系统策略', '2025-09-24 11:38:01', '2025-09-24 11:38:01');
|
||||||
|
INSERT INTO `score_records` VALUES (11, 6, 9, '系统策略', '2025-09-24 11:43:59', '2025-09-24 11:43:59');
|
||||||
|
INSERT INTO `score_records` VALUES (12, 6, 9, '系统策略', '2025-09-24 11:45:30', '2025-09-24 11:45:30');
|
||||||
|
INSERT INTO `score_records` VALUES (13, 1, 9, '系统策略', '2025-12-20 16:01:51', '2025-12-20 16:01:51');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for secondary_products
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `secondary_products`;
|
||||||
|
CREATE TABLE `secondary_products` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`seller_id` bigint UNSIGNED NOT NULL COMMENT '卖家用户ID',
|
||||||
|
`original_product_id` bigint NULL DEFAULT NULL COMMENT '原始商品ID(一级商品ID或二级商品ID)',
|
||||||
|
`original_product_type` tinyint NOT NULL COMMENT '原始商品类型:1一级商品,2二级商品',
|
||||||
|
`product_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称',
|
||||||
|
`product_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品描述',
|
||||||
|
`product_images` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品图片URLs,JSON格式存储',
|
||||||
|
`cost_price` decimal(10, 2) NOT NULL COMMENT '成本价(用户购买时的价格)',
|
||||||
|
`selling_price` decimal(10, 2) NOT NULL COMMENT '出售价格',
|
||||||
|
`quantity` bigint NOT NULL DEFAULT 1 COMMENT '出售数量',
|
||||||
|
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0下架,1上架,2已售出',
|
||||||
|
`view_count` bigint NOT NULL DEFAULT 0 COMMENT '浏览量',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`category_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '商品分类ID',
|
||||||
|
`product_condition` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`user_warehouse_id` bigint UNSIGNED NOT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `fk_secondary_products_seller`(`seller_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_secondary_products_category`(`category_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_secondary_products_user_warehouse`(`user_warehouse_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_secondary_products_category` FOREIGN KEY (`category_id`) REFERENCES `product_categories` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_secondary_products_seller` FOREIGN KEY (`seller_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `fk_secondary_products_user_warehouse` FOREIGN KEY (`user_warehouse_id`) REFERENCES `user_warehouse` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of secondary_products
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `secondary_products` VALUES (1, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 899.00, 1, 0, 10, '2025-09-03 14:02:19', '2025-09-04 09:41:59', 1, '九成新', 3);
|
||||||
|
INSERT INTO `secondary_products` VALUES (2, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 888.00, 0, 1, 11, '2025-09-03 15:09:40', '2025-09-10 13:33:29', 1, '九成新', 2);
|
||||||
|
INSERT INTO `secondary_products` VALUES (3, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 976.80, 2, 0, 1, '2025-09-04 09:43:30', '2025-09-04 09:51:46', 1, '九成新', 1);
|
||||||
|
INSERT INTO `secondary_products` VALUES (4, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 1, 17, '2025-09-04 09:53:50', '2025-09-24 11:31:43', 1, '九成新', 3);
|
||||||
|
INSERT INTO `secondary_products` VALUES (5, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 1, 12, '2025-09-04 09:53:53', '2025-09-13 12:57:30', 1, '九成新', 1);
|
||||||
|
INSERT INTO `secondary_products` VALUES (6, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 1, 6, '2025-09-13 12:52:30', '2025-09-13 12:57:23', 1, '九成新', 1);
|
||||||
|
INSERT INTO `secondary_products` VALUES (7, 8, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 1, 5, '2025-09-13 14:18:51', '2025-09-13 15:23:16', 1, '九成新', 4);
|
||||||
|
INSERT INTO `secondary_products` VALUES (8, 7, 7, 2, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 870.00, 0, 1, 4, '2025-09-13 16:28:41', '2025-09-13 16:31:06', 1, '九成新', 5);
|
||||||
|
INSERT INTO `secondary_products` VALUES (9, 7, 1, 1, '酒水', '酒水 - 来自个人仓库123', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 1, 3, '2025-09-13 16:34:24', '2025-09-13 16:34:58', 1, '九成新', 6);
|
||||||
|
INSERT INTO `secondary_products` VALUES (10, 7, 2, 1, '测试', '测试 - 来自个人仓库', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 1, 4, '2025-09-16 12:10:21', '2025-09-24 11:03:36', 3, '九成新', 8);
|
||||||
|
INSERT INTO `secondary_products` VALUES (11, 6, 1, 1, '酒水', '酒水 - 来自个人仓库', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 1, 4, '2025-09-24 11:35:19', '2025-09-24 11:36:15', 4, '九成新', 9);
|
||||||
|
INSERT INTO `secondary_products` VALUES (12, 1, 2, 1, '测试', '测试 - 来自个人仓库', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 1, 1, 1, '2025-12-20 15:30:37', '2025-12-20 16:08:39', 3, '九成新', 11);
|
||||||
|
INSERT INTO `secondary_products` VALUES (13, 1, 2, 1, '测试', '测试 - 来自个人仓库', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 1, 3, '2025-12-20 15:30:38', '2025-12-20 16:06:29', 3, '九成新', 13);
|
||||||
|
INSERT INTO `secondary_products` VALUES (14, 1, 2, 1, '测试', '测试 - 来自个人仓库', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 1, 3, '2025-12-20 15:30:40', '2025-12-20 15:34:53', 3, '九成新', 12);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for system_configs
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `system_configs`;
|
||||||
|
CREATE TABLE `system_configs` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`config_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置键',
|
||||||
|
`config_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置值',
|
||||||
|
`config_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置名称',
|
||||||
|
`config_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配置描述',
|
||||||
|
`config_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'string' COMMENT '配置类型:string、number、boolean、json',
|
||||||
|
`is_system` tinyint NOT NULL DEFAULT 0 COMMENT '是否系统配置:0否,1是',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `idx_system_configs_config_key`(`config_key` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of system_configs
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `system_configs` VALUES (1, 'warehouse_price_rate', '-9', '入库价格增值(负数为减少,正数为增加)', '商品进入用户仓库时的价格增值', 'number', 1, '2025-09-02 08:16:52', '2025-09-10 12:28:33');
|
||||||
|
INSERT INTO `system_configs` VALUES (6, 'order_over_time', '25', '订单超时时间', '订单超时时间(分钟)', 'number', 1, '2025-09-03 15:50:15', '2025-09-04 08:59:19');
|
||||||
|
INSERT INTO `system_configs` VALUES (7, 'phone', '12345678913', '客服电话', '', 'string', 1, '2025-09-03 15:53:02', '2025-09-04 08:58:54');
|
||||||
|
INSERT INTO `system_configs` VALUES (8, 'limit_days', '0', '买家限制天数(0 为不限制)', '限制天数(0 为不限制)', 'string', 1, '2025-09-04 08:52:16', '2025-12-20 16:08:15');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for user_addresses
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `user_addresses`;
|
||||||
|
CREATE TABLE `user_addresses` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint UNSIGNED NOT NULL COMMENT '用户ID',
|
||||||
|
`consignee_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收件人姓名',
|
||||||
|
`consignee_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收件人电话',
|
||||||
|
`province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '省份',
|
||||||
|
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '城市',
|
||||||
|
`district` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '区县',
|
||||||
|
`detail_address` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '详细地址',
|
||||||
|
`postal_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮政编码',
|
||||||
|
`is_default` tinyint NOT NULL DEFAULT 0 COMMENT '是否默认地址:0否,1是',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `fk_user_addresses_user`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_user_addresses_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of user_addresses
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `user_addresses` VALUES (1, 8, '牛马牛马', '18369004078', '北京市', '北京市', '东城区', '阿斯顿撒旦撒大苏打', '', 0, '2025-09-03 11:13:51', '2025-09-13 20:48:10');
|
||||||
|
INSERT INTO `user_addresses` VALUES (2, 6, '牛阿斯顿', '18661900405', '北京市', '北京市', '东城区', 'sadsadsad', '', 1, '2025-09-03 14:15:48', '2025-09-03 14:15:48');
|
||||||
|
INSERT INTO `user_addresses` VALUES (3, 7, 'sad', '18361900405', '北京市', '北京市', '东城区', 'sadasd', '', 1, '2025-09-03 15:11:49', '2025-09-03 15:11:49');
|
||||||
|
INSERT INTO `user_addresses` VALUES (4, 8, '周天周天', '18961900403', '北京市', '市辖区', '朝阳区', '阿斯顿撒旦撒大苏打', '', 1, '2025-09-13 20:48:10', '2025-09-13 20:48:10');
|
||||||
|
INSERT INTO `user_addresses` VALUES (5, 1, '乱说的', '18361940401', '北京市', '市辖区', '东城区', 'asd', 'asdasd', 1, '2025-12-20 15:26:53', '2025-12-20 15:26:53');
|
||||||
|
INSERT INTO `user_addresses` VALUES (6, 2, 'wesr', '18361900405', '北京市', '市辖区', '东城区', 'sadf', 'asd', 1, '2025-12-20 15:34:50', '2025-12-20 15:34:50');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for user_warehouse
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `user_warehouse`;
|
||||||
|
CREATE TABLE `user_warehouse` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint UNSIGNED NOT NULL COMMENT '用户ID',
|
||||||
|
`product_id` bigint NOT NULL COMMENT '商品ID',
|
||||||
|
`product_type` tinyint NOT NULL COMMENT '商品类型:1一级商品,2二级商品',
|
||||||
|
`product_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称',
|
||||||
|
`product_images` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品图片URLs,JSON格式存储',
|
||||||
|
`purchase_price` decimal(10, 2) NOT NULL COMMENT '购买价格',
|
||||||
|
`warehouse_price` decimal(10, 2) NOT NULL COMMENT '入库价格(根据系统常量增值后)',
|
||||||
|
`quantity` bigint NOT NULL DEFAULT 1 COMMENT '数量',
|
||||||
|
`source_order_id` bigint NULL DEFAULT NULL COMMENT '来源订单ID',
|
||||||
|
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0已出售,1库存中',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `fk_user_warehouse_user`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_user_warehouse_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of user_warehouse
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `user_warehouse` VALUES (1, 8, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 1, 2, 1, '2025-09-03 13:25:20', '2025-09-13 12:52:31');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (2, 8, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 888.00, 0, 4, 0, '2025-09-03 13:44:35', '2025-09-03 15:09:40');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (3, 8, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 3, 5, 1, '2025-09-03 13:48:40', '2025-09-04 09:53:50');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (4, 8, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 13, 0, '2025-09-13 12:52:02', '2025-09-13 14:18:52');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (5, 7, 7, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 870.00, 0, 17, 0, '2025-09-13 16:26:38', '2025-09-13 16:28:42');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (6, 7, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 0, 20, 0, '2025-09-13 16:34:03', '2025-09-13 16:34:25');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (7, 8, 9, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 870.00, 1, 21, 1, '2025-09-13 16:35:27', '2025-09-13 16:35:27');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (8, 7, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 22, 0, '2025-09-16 12:10:09', '2025-09-16 12:10:21');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (9, 6, 1, 1, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 888.00, 879.00, 9, 25, 1, '2025-09-24 11:33:58', '2025-09-24 11:35:19');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (10, 7, 11, 2, '酒水', '/back/file/20250902104133_a0ac0a17.jpeg', 879.00, 870.00, 1, 26, 1, '2025-09-24 11:45:30', '2025-09-24 11:45:30');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (11, 1, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 27, 0, '2025-12-20 15:29:00', '2025-12-20 15:30:37');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (12, 1, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 29, 0, '2025-12-20 15:30:05', '2025-12-20 15:30:40');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (13, 1, 2, 1, '测试', '/back/file/20250916115526_fc06f62c.png', 77.00, 68.00, 0, 28, 0, '2025-12-20 15:30:08', '2025-12-20 15:30:39');
|
||||||
|
INSERT INTO `user_warehouse` VALUES (14, 2, 14, 2, '测试', '/back/file/20250916115526_fc06f62c.png', 68.00, 59.00, 1, 31, 1, '2025-12-20 16:01:51', '2025-12-20 16:01:51');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for users
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `users`;
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`system_role` tinyint NOT NULL DEFAULT 2 COMMENT '系统角色:0管理员,1代理商,2普通用户',
|
||||||
|
`customer_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`current_points` bigint NOT NULL DEFAULT 0 COMMENT '当前积分',
|
||||||
|
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`company_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`personal_intro` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`identity_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`referrer_identity_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`business_license_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`id_card_front_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`id_card_back_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`main_payment_qr_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`sub_payment_qr_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0禁用,1启用',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`expiry_date` timestamp NULL DEFAULT NULL COMMENT '过期时间,null表示永不过期',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `idx_users_phone`(`phone` ASC) USING BTREE,
|
||||||
|
UNIQUE INDEX `idx_users_identity_code`(`identity_code` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of users
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `users` VALUES (1, '13800138000', 0, '系统管理员', 'Administrator', 9, '/back/file/20250902112107_639669b0.png', '', '', 'A00000001', '', '', '', '', '/back/file/20250903115327_af655669.jpeg', '/back/file/20250903115330_ea22e0d9.jpeg', 'e10adc3949ba59abbe56e057f20f883e', 1, '2025-09-02 08:16:52', '2025-12-20 16:01:52', NULL);
|
||||||
|
INSERT INTO `users` VALUES (2, '13800138001', 1, '测试代理商', 'Test Agent', 0, '', '', '', 'T00000002', '', '', '', '', '', '', 'e10adc3949ba59abbe56e057f20f883e', 1, '2025-09-02 08:16:52', '2025-09-13 16:51:06', NULL);
|
||||||
|
INSERT INTO `users` VALUES (3, '13800138002', 2, '测试用户1', 'Test User 1', 0, '', '', '', 'U00000003', 'T00000002', '', '', '', '', '', '6ad14ba9986e3615423dfca256d04e3f', 1, '2025-09-02 08:16:53', '2025-09-02 08:16:53', NULL);
|
||||||
|
INSERT INTO `users` VALUES (4, '13800138003', 2, '测试用户2', 'Test User 2', 5, '', '', '', 'U00000004', 'T00000002', '', '', '', '', '', '6ad14ba9986e3615423dfca256d04e3f', 1, '2025-09-02 08:16:53', '2025-09-02 09:00:34', NULL);
|
||||||
|
INSERT INTO `users` VALUES (5, '18361900404', 2, '李白', '李白', 5, '', '里吧科技', '', '', '', '', '', '', '', '', 'e10adc3949ba59abbe56e057f20f883e', 1, '2025-09-02 12:25:42', '2025-09-03 16:32:09', NULL);
|
||||||
|
INSERT INTO `users` VALUES (6, '18361900401', 2, '咯i', '年少的', 27, '', '阿三的', '', 'Ud853ac93', '', '', '', '', '', '', 'e10adc3949ba59abbe56e057f20f883e', 1, '2025-09-02 12:25:42', '2025-09-24 11:45:30', NULL);
|
||||||
|
INSERT INTO `users` VALUES (7, '18961900405', 1, '牛马', '牛马', 27, '', '阿三的', '', 'U00000007', 'T00000002', '/back/file/20250902150542_3a4a5b13.png', '/back/file/20250902150503_da9e6bd8.jpg', '/back/file/20250902150539_a082ce87.png', '', '', 'e10adc3949ba59abbe56e057f20f883e', 1, '2025-09-02 15:06:29', '2025-10-23 17:46:04', NULL);
|
||||||
|
INSERT INTO `users` VALUES (8, '13225208566', 2, '牛马', '张三', 2509, '/back/file/20250916114909_dededd0c.png', '阿三科技', '', 'U7f27a121', 'T00000002', '', '/back/file/20250903085714_da9e6bd8.jpg', '', '', '', 'e10adc3949ba59abbe56e057f20f883e', 1, '2025-09-03 08:57:43', '2025-12-20 16:06:15', '2025-09-30 00:00:00');
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/events"
|
||||||
|
"awesomeProject/internal/handlers"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"awesomeProject/internal/routers"
|
||||||
|
"awesomeProject/internal/tasks"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 1. 加载配置文件
|
||||||
|
config, err := common.LoadConfig("./configs/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("加载配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("配置加载成功: %s v%s", config.App.Name, config.App.Version)
|
||||||
|
|
||||||
|
// 2. 初始化数据库
|
||||||
|
if err := common.InitDatabase(&config.Database); err != nil {
|
||||||
|
log.Fatalf("数据库初始化失败: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := common.CloseDatabase()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 4. 自动迁移数据表
|
||||||
|
if err := models.AutoMigrateAll(); err != nil {
|
||||||
|
log.Fatalf("数据表迁移失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("数据表迁移完成")
|
||||||
|
|
||||||
|
// 5. 初始化系统配置
|
||||||
|
//configHandler := &handlers.AdminConfigHandler{}
|
||||||
|
//if err := configHandler.InitSystemConfigs(); err != nil {
|
||||||
|
// log.Fatalf("初始化系统配置失败: %v", err)
|
||||||
|
//}
|
||||||
|
//log.Println("系统配置初始化完成")
|
||||||
|
|
||||||
|
// 6. 初始化事件系统
|
||||||
|
events.InitEventSystem()
|
||||||
|
|
||||||
|
// 7. 初始化定时任务
|
||||||
|
tasks.InitCronTasks()
|
||||||
|
log.Println("定时任务初始化完成")
|
||||||
|
|
||||||
|
// 8. 初始化默认用户
|
||||||
|
initHandler := &handlers.InitHandler{}
|
||||||
|
if err := initHandler.InitAll(); err != nil {
|
||||||
|
log.Fatalf("初始化默认用户失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("默认用户初始化完成")
|
||||||
|
|
||||||
|
// 9. 设置 Gin 模式
|
||||||
|
if config.App.Env == "production" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 创建Gin路由器和设置路由
|
||||||
|
router := routers.SetupRouter()
|
||||||
|
|
||||||
|
// 配置静态资源
|
||||||
|
// 上传文件的静态资源(允许缓存)
|
||||||
|
router.Static("/back/file", "./public/file")
|
||||||
|
|
||||||
|
// H5 静态资源
|
||||||
|
router.Static("/h5/static", "./public/h5/static")
|
||||||
|
router.Static("/h5/assets", "./public/h5/assets")
|
||||||
|
|
||||||
|
// Vue 静态资源(禁用缓存)
|
||||||
|
webGroup := router.Group("/")
|
||||||
|
webGroup.Use(func(c *gin.Context) {
|
||||||
|
// 只对web静态资源禁用缓存
|
||||||
|
if strings.HasPrefix(c.Request.URL.Path, "/assets") ||
|
||||||
|
strings.HasPrefix(c.Request.URL.Path, "/css") ||
|
||||||
|
strings.HasPrefix(c.Request.URL.Path, "/js") {
|
||||||
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
c.Header("Pragma", "no-cache")
|
||||||
|
c.Header("Expires", "0")
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
webGroup.Static("/assets", "./public/web/assets")
|
||||||
|
webGroup.Static("/css", "./public/web/css")
|
||||||
|
webGroup.Static("/js", "./public/web/js")
|
||||||
|
|
||||||
|
// H5 History 路由支持 - /h5 开头的路由返回 H5 的 index.html
|
||||||
|
router.NoRoute(func(c *gin.Context) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
|
// 如果是 API 请求(/back 开头),返回 404
|
||||||
|
if strings.HasPrefix(path, "/back") {
|
||||||
|
c.JSON(404, gin.H{"error": "API not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// H5 路由处理
|
||||||
|
if strings.HasPrefix(path, "/h5") {
|
||||||
|
// 检查是否是静态文件(包含文件扩展名)
|
||||||
|
if strings.Contains(path, ".") {
|
||||||
|
// 静态文件应该已经被上面的 Static 路由处理了
|
||||||
|
// 如果到这里说明文件不存在,返回 404
|
||||||
|
c.JSON(404, gin.H{"error": "File not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// H5 路由返回 H5 的 index.html
|
||||||
|
c.File("./public/h5/index.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是静态文件(包含文件扩展名)
|
||||||
|
if strings.Contains(path, ".") {
|
||||||
|
// 设置禁用缓存的响应头
|
||||||
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
c.Header("Pragma", "no-cache")
|
||||||
|
c.Header("Expires", "0")
|
||||||
|
|
||||||
|
// 尝试访问 web 目录下的文件
|
||||||
|
c.File("./public/web" + path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他路由返回 Vue 应用
|
||||||
|
c.File("./public/web/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
port := fmt.Sprintf(":%d", config.App.Port)
|
||||||
|
log.Printf("服务器启动在端口 %s", port)
|
||||||
|
if err := router.Run(port); err != nil {
|
||||||
|
log.Fatalf("服务器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 应用配置
|
||||||
|
app:
|
||||||
|
name: "AwesomeProject"
|
||||||
|
version: "1.0.0"
|
||||||
|
port: 8090
|
||||||
|
env: "development" # development, production, test
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
database:
|
||||||
|
driver: "mysql" # mysql, postgres, sqlite
|
||||||
|
host: "localhost"
|
||||||
|
port: 3306
|
||||||
|
username: "root"
|
||||||
|
password: "123456"
|
||||||
|
dbname: "a_business_help"
|
||||||
|
charset: "utf8mb4"
|
||||||
|
# SQLite 配置 (当 driver 为 sqlite 时使用)
|
||||||
|
sqlite_path: "./data/awesome.db"
|
||||||
|
# 连接池配置
|
||||||
|
max_idle_conns: 10
|
||||||
|
max_open_conns: 100
|
||||||
|
conn_max_lifetime: 3600 # 秒
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
jwt:
|
||||||
|
secret_key: "your-very-secret-key-32-bytes-long-string"
|
||||||
|
expire_hours: 24
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
server:
|
||||||
|
read_timeout: 60 # 秒
|
||||||
|
write_timeout: 60 # 秒
|
||||||
|
static_path: "./public"
|
||||||
|
upload_max_size: 100 # MB
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
log:
|
||||||
|
level: "info" # debug, info, warn, error
|
||||||
|
file_path: "./logs/app.log"
|
||||||
|
max_size: 100 # MB
|
||||||
|
max_age: 30 # 天
|
||||||
|
max_backups: 10 # 文件数量
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# untitled
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# 管理员端前端项目
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
这是一个基于Vue 3 + Element Plus的管理员端前端项目,用于管理虚拟商品交易平台。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **框架**: Vue 3 (Composition API)
|
||||||
|
- **UI库**: Element Plus
|
||||||
|
- **状态管理**: Pinia
|
||||||
|
- **路由**: Vue Router 4
|
||||||
|
- **HTTP客户端**: Axios
|
||||||
|
- **图表**: ECharts
|
||||||
|
- **构建工具**: Vite
|
||||||
|
- **样式**: CSS3 + Tailwind CSS
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── layouts/ # 布局组件
|
||||||
|
│ └── AdminLayout.vue # 管理端主布局
|
||||||
|
├── views/ # 页面组件
|
||||||
|
│ ├── Login.vue # 登录页面
|
||||||
|
│ └── admin/ # 管理端页面
|
||||||
|
│ ├── Dashboard.vue # 仪表盘
|
||||||
|
│ ├── Users.vue # 用户管理
|
||||||
|
│ ├── Scores.vue # 积分管理
|
||||||
|
│ └── orders/ # 订单管理
|
||||||
|
│ ├── PurchaseOrders.vue # 买单管理
|
||||||
|
│ └── SalesOrders.vue # 卖单管理
|
||||||
|
├── stores/ # Pinia状态管理
|
||||||
|
│ └── auth.js # 认证状态
|
||||||
|
├── router/ # 路由配置
|
||||||
|
│ └── index.js # 主路由文件
|
||||||
|
└── main.js # 应用入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 🔐 认证系统
|
||||||
|
- 支持管理员和代理商登录
|
||||||
|
- JWT Token认证
|
||||||
|
- 自动路由守卫
|
||||||
|
- 权限控制
|
||||||
|
|
||||||
|
### 👥 用户管理
|
||||||
|
- 用户列表查看(支持搜索、筛选、分页)
|
||||||
|
- 用户详情查看
|
||||||
|
- 用户创建、编辑、删除(仅管理员)
|
||||||
|
- 用户状态管理
|
||||||
|
- 权限级别显示
|
||||||
|
|
||||||
|
### 📋 订单管理
|
||||||
|
- 买单管理(查看、处理订单状态)
|
||||||
|
- 卖单管理(查看、处理订单状态)
|
||||||
|
- 订单详情查看
|
||||||
|
- 支持多条件筛选
|
||||||
|
- 订单状态流转
|
||||||
|
|
||||||
|
### ⭐ 积分管理
|
||||||
|
- 积分记录查看(支持搜索、筛选)
|
||||||
|
- 积分记录创建、编辑、删除(仅管理员)
|
||||||
|
- 积分变化统计
|
||||||
|
- 用户积分查询
|
||||||
|
|
||||||
|
### 📊 数据统计
|
||||||
|
- 仪表盘数据概览
|
||||||
|
- 用户角色分布图表
|
||||||
|
- 订单趋势图表
|
||||||
|
- 实时统计数据
|
||||||
|
|
||||||
|
### 🎨 界面特性
|
||||||
|
- 响应式设计,支持移动端
|
||||||
|
- 现代化UI设计
|
||||||
|
- 暗色侧边栏
|
||||||
|
- 面包屑导航
|
||||||
|
- 页面切换动画
|
||||||
|
- 数据加载状态
|
||||||
|
|
||||||
|
## 权限说明
|
||||||
|
|
||||||
|
### 管理员权限(system_role=0)
|
||||||
|
- ✅ 访问所有功能模块
|
||||||
|
- ✅ 用户管理(查看、创建、编辑、删除)
|
||||||
|
- ✅ 订单管理(查看、处理)
|
||||||
|
- ✅ 积分管理(查看、创建、编辑、删除)
|
||||||
|
- ✅ 商品管理(计划中)
|
||||||
|
- ✅ 系统配置(计划中)
|
||||||
|
|
||||||
|
### 代理商权限(system_role=1)
|
||||||
|
- ✅ 仪表盘查看(仅自己邀请的用户数据)
|
||||||
|
- ✅ 用户管理(仅查看自己邀请的用户)
|
||||||
|
- ✅ 订单管理(仅查看相关用户的订单)
|
||||||
|
- ✅ 积分管理(仅查看相关用户的积分记录)
|
||||||
|
- ❌ 无创建、编辑、删除权限
|
||||||
|
- ❌ 无商品管理权限
|
||||||
|
- ❌ 无系统配置权限
|
||||||
|
|
||||||
|
## 安装和运行
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
```bash
|
||||||
|
cd front
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动开发服务器
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 构建生产版本
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 默认测试账户
|
||||||
|
|
||||||
|
### 管理员账户
|
||||||
|
- 手机号:`13800138000`
|
||||||
|
- 密码:`admin123`
|
||||||
|
- 权限:全部功能
|
||||||
|
|
||||||
|
### 代理商账户
|
||||||
|
- 手机号:`13800138001`
|
||||||
|
- 密码:`agent123`
|
||||||
|
- 权限:受限查看
|
||||||
|
|
||||||
|
## API配置
|
||||||
|
|
||||||
|
项目已配置axios默认baseURL为`/back`,确保后端API正确运行在对应端口。
|
||||||
|
|
||||||
|
请求拦截器会自动添加token到请求头:
|
||||||
|
```javascript
|
||||||
|
headers: {
|
||||||
|
token: localStorage.getItem('token')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应拦截器会自动处理401未授权错误,跳转到登录页面。
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 组件命名
|
||||||
|
- 页面组件:PascalCase(如:`Users.vue`)
|
||||||
|
- 布局组件:PascalCase + Layout后缀(如:`AdminLayout.vue`)
|
||||||
|
- 通用组件:PascalCase(如:`DataTable.vue`)
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
- 使用Pinia进行状态管理
|
||||||
|
- 按功能模块划分store
|
||||||
|
- 使用Composition API风格
|
||||||
|
|
||||||
|
### 样式规范
|
||||||
|
- 优先使用Element Plus组件样式
|
||||||
|
- 使用scoped CSS避免样式污染
|
||||||
|
- 响应式设计,移动端适配
|
||||||
|
- 使用CSS变量统一主题色彩
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 使用Composition API
|
||||||
|
- 使用TypeScript类型注解(计划中)
|
||||||
|
- 函数和变量使用camelCase命名
|
||||||
|
- 常量使用UPPER_SNAKE_CASE命名
|
||||||
|
|
||||||
|
## 待开发功能
|
||||||
|
|
||||||
|
- [ ] 商品管理页面
|
||||||
|
- [ ] 系统配置管理
|
||||||
|
- [ ] 数据导出功能
|
||||||
|
- [ ] 批量操作功能
|
||||||
|
- [ ] 个人信息管理
|
||||||
|
- [ ] 操作日志记录
|
||||||
|
- [ ] 系统通知功能
|
||||||
|
- [ ] 文件上传组件
|
||||||
|
- [ ] 富文本编辑器
|
||||||
|
- [ ] 国际化支持
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **权限控制**:代理商只能看到自己邀请的用户相关数据
|
||||||
|
2. **数据过滤**:前端路由守卫 + 后端API权限双重保护
|
||||||
|
3. **错误处理**:统一的错误提示和处理机制
|
||||||
|
4. **性能优化**:按需加载、虚拟滚动等优化措施
|
||||||
|
5. **安全性**:Token自动过期处理、XSS防护
|
||||||
|
|
||||||
|
## 浏览器支持
|
||||||
|
|
||||||
|
- Chrome >= 87
|
||||||
|
- Firefox >= 78
|
||||||
|
- Safari >= 14
|
||||||
|
- Edge >= 88
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
1. Fork项目
|
||||||
|
2. 创建特性分支
|
||||||
|
3. 提交更改
|
||||||
|
4. 推送到分支
|
||||||
|
5. 创建Pull Request
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/icon.jpg">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>后台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Generated
+9670
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "untitled",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||||
|
"@antv/g6": "^5.0.47",
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"@kangc/v-md-editor": "^2.3.18",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"chart.js": "^4.4.7",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"element-plus": "^2.5.6",
|
||||||
|
"gojs": "^3.0.22",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
|
"neo4j-driver": "^5.28.1",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-awesome-swiper": "^5.0.1",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"unplugin-auto-import": "^0.17.5",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"vite": "^6.2.4",
|
||||||
|
"vite-plugin-proxy": "^0.5.0",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#409eff;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#67c23a;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景圆形 -->
|
||||||
|
<circle cx="16" cy="16" r="15" fill="url(#logoGradient)" stroke="#fff" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- 商品图标 -->
|
||||||
|
<g fill="#fff">
|
||||||
|
<!-- 购物袋主体 -->
|
||||||
|
<path d="M9 12h14v12a2 2 0 0 1-2 2H11a2 2 0 0 1-2-2V12z"/>
|
||||||
|
<!-- 购物袋手柄 -->
|
||||||
|
<path d="M12 8a4 4 0 0 1 8 0v4h-2V8a2 2 0 0 0-4 0v4h-2V8z"/>
|
||||||
|
<!-- 装饰点 -->
|
||||||
|
<circle cx="13" cy="18" r="1"/>
|
||||||
|
<circle cx="19" cy="18" r="1"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 798 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,3 @@
|
|||||||
|
/*@tailwind base;*/
|
||||||
|
/*@tailwind components;*/
|
||||||
|
/*@tailwind utilities;*/
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-upload">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:action="uploadUrl"
|
||||||
|
:headers="headers"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-success="onSuccess"
|
||||||
|
:on-error="onError"
|
||||||
|
:on-remove="onRemove"
|
||||||
|
:limit="limit"
|
||||||
|
:multiple="multiple"
|
||||||
|
:show-file-list="false"
|
||||||
|
:auto-upload="autoUpload"
|
||||||
|
accept="image/*"
|
||||||
|
class="upload-demo"
|
||||||
|
>
|
||||||
|
<div v-if="imageUrls.length < limit && !(props.previewSrcList && props.previewSrcList.length > 0 && !props.multiple)" class="upload-item">
|
||||||
|
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||||
|
<div class="upload-text">上传图片</div>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<div v-if="imageUrls.length > 0 || (props.previewSrcList && props.previewSrcList.length > 0)" class="image-list">
|
||||||
|
<!-- 显示已有图片 -->
|
||||||
|
<div
|
||||||
|
v-for="(url, index) in imageUrls"
|
||||||
|
:key="`uploaded-${index}`"
|
||||||
|
class="image-item"
|
||||||
|
>
|
||||||
|
<el-image
|
||||||
|
:src="getImageUrl(url)"
|
||||||
|
:preview-src-list="props.previewSrcList.length > 0 ? props.previewSrcList : imageUrls.map(u => getImageUrl(u))"
|
||||||
|
:initial-index="index"
|
||||||
|
fit="cover"
|
||||||
|
class="image-preview"
|
||||||
|
/>
|
||||||
|
<div class="image-actions">
|
||||||
|
<el-icon class="action-icon" @click="previewImage(index)">
|
||||||
|
<ZoomIn />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon class="action-icon" @click="removeImage(index)">
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 显示外部传入的预览图片(当没有上传图片时) -->
|
||||||
|
<div
|
||||||
|
v-if="imageUrls.length === 0 && props.previewSrcList && props.previewSrcList.length > 0"
|
||||||
|
v-for="(url, index) in props.previewSrcList"
|
||||||
|
:key="`preview-${index}`"
|
||||||
|
class="image-item"
|
||||||
|
>
|
||||||
|
<el-image
|
||||||
|
:src="url"
|
||||||
|
:preview-src-list="props.previewSrcList"
|
||||||
|
:initial-index="index"
|
||||||
|
fit="cover"
|
||||||
|
class="image-preview"
|
||||||
|
/>
|
||||||
|
<div class="image-actions">
|
||||||
|
<el-icon class="action-icon" @click="previewImage(index)">
|
||||||
|
<ZoomIn />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon class="action-icon" @click="removeExternalImage(index)">
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传进度 -->
|
||||||
|
<el-progress
|
||||||
|
v-if="uploading"
|
||||||
|
:percentage="uploadProgress"
|
||||||
|
:show-text="false"
|
||||||
|
class="upload-progress"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 图片预览器 -->
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="showViewer"
|
||||||
|
:url-list="viewerUrls"
|
||||||
|
:initial-index="viewerIndex"
|
||||||
|
@close="closeViewer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Array],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 9
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autoUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 10 // MB
|
||||||
|
},
|
||||||
|
previewSrcList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const uploadRef = ref()
|
||||||
|
const fileList = ref([])
|
||||||
|
const uploading = ref(false)
|
||||||
|
const uploadProgress = ref(0)
|
||||||
|
const showViewer = ref(false)
|
||||||
|
const viewerIndex = ref(0)
|
||||||
|
const viewerUrls = ref([])
|
||||||
|
|
||||||
|
const uploadUrl = '/back/upload'
|
||||||
|
const headers = computed(() => ({
|
||||||
|
token: localStorage.getItem('token') || ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 图片URL列表
|
||||||
|
const imageUrls = computed({
|
||||||
|
get() {
|
||||||
|
if (!props.modelValue) return []
|
||||||
|
if (typeof props.modelValue === 'string') {
|
||||||
|
return props.modelValue ? props.modelValue.split(',').filter(url => url.trim()) : []
|
||||||
|
}
|
||||||
|
return props.modelValue || []
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const newValue = props.multiple ? value.join(',') : (value[0] || '')
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
|
emit('change', newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取完整图片URL
|
||||||
|
const getImageUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${window.location.origin}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传前检查
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
// 检查文件类型
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小
|
||||||
|
const isLtMaxSize = file.size / 1024 / 1024 < props.maxSize
|
||||||
|
if (!isLtMaxSize) {
|
||||||
|
ElMessage.error(`图片大小不能超过 ${props.maxSize}MB!`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数量限制
|
||||||
|
if (imageUrls.value.length >= props.limit) {
|
||||||
|
ElMessage.error(`最多只能上传 ${props.limit} 张图片!`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true
|
||||||
|
uploadProgress.value = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传成功
|
||||||
|
const onSuccess = (response, file) => {
|
||||||
|
uploading.value = false
|
||||||
|
uploadProgress.value = 100
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const newUrls = [...imageUrls.value, response.data.url]
|
||||||
|
imageUrls.value = newUrls
|
||||||
|
ElMessage.success('图片上传成功')
|
||||||
|
|
||||||
|
// 清空上传组件的文件列表,避免下次上传时出现问题
|
||||||
|
if (uploadRef.value) {
|
||||||
|
uploadRef.value.clearFiles()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传失败
|
||||||
|
const onError = (error, file) => {
|
||||||
|
uploading.value = false
|
||||||
|
uploadProgress.value = 0
|
||||||
|
console.error('上传失败:', error)
|
||||||
|
ElMessage.error('图片上传失败,请重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除文件
|
||||||
|
const onRemove = (file) => {
|
||||||
|
// 这里可以实现从服务器删除文件的逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除图片
|
||||||
|
const removeImage = (index) => {
|
||||||
|
const newUrls = [...imageUrls.value]
|
||||||
|
newUrls.splice(index, 1)
|
||||||
|
imageUrls.value = newUrls
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览图片
|
||||||
|
const previewImage = (index) => {
|
||||||
|
// 设置预览图片列表和初始索引
|
||||||
|
if (props.previewSrcList && props.previewSrcList.length > 0) {
|
||||||
|
viewerUrls.value = props.previewSrcList
|
||||||
|
} else {
|
||||||
|
viewerUrls.value = imageUrls.value.map(url => getImageUrl(url))
|
||||||
|
}
|
||||||
|
viewerIndex.value = index
|
||||||
|
showViewer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭图片预览
|
||||||
|
const closeViewer = () => {
|
||||||
|
showViewer.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除外部传入的图片
|
||||||
|
const removeExternalImage = (index) => {
|
||||||
|
// 通知父组件删除外部图片
|
||||||
|
emit('update:modelValue', '')
|
||||||
|
emit('change', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动上传
|
||||||
|
const submit = () => {
|
||||||
|
if (uploadRef.value) {
|
||||||
|
uploadRef.value.submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空文件
|
||||||
|
const clearFiles = () => {
|
||||||
|
if (uploadRef.value) {
|
||||||
|
uploadRef.value.clearFiles()
|
||||||
|
}
|
||||||
|
imageUrls.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听外部传入的值变化,同步重置内部状态
|
||||||
|
watch(() => props.modelValue, (newValue, oldValue) => {
|
||||||
|
// 当外部值从有值变为空值时,清空内部状态
|
||||||
|
if ((oldValue && !newValue) || (oldValue && newValue === '')) {
|
||||||
|
if (uploadRef.value) {
|
||||||
|
uploadRef.value.clearFiles()
|
||||||
|
}
|
||||||
|
fileList.value = []
|
||||||
|
uploading.value = false
|
||||||
|
uploadProgress.value = 0
|
||||||
|
}
|
||||||
|
}, { immediate: false })
|
||||||
|
|
||||||
|
// 暴露方法供父组件调用
|
||||||
|
defineExpose({
|
||||||
|
submit,
|
||||||
|
clearFiles
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-upload {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-demo {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item {
|
||||||
|
width: 148px;
|
||||||
|
height: 148px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-text {
|
||||||
|
color: #8c939d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
position: relative;
|
||||||
|
width: 148px;
|
||||||
|
height: 148px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item:hover .image-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-layout">
|
||||||
|
<el-container>
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<el-aside :width="collapsed ? '64px' : '250px'" class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<img v-if="!collapsed" src="/logo.svg" alt="Logo" class="logo-img">
|
||||||
|
<span v-if="!collapsed" class="logo-text">管理后台</span>
|
||||||
|
<el-icon v-else class="logo-icon"><Menu /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-menu
|
||||||
|
:default-active="$route.path"
|
||||||
|
:collapse="collapsed"
|
||||||
|
:unique-opened="true"
|
||||||
|
router
|
||||||
|
class="sidebar-menu"
|
||||||
|
background-color="#304156"
|
||||||
|
text-color="#bfcbd9"
|
||||||
|
active-text-color="#409eff"
|
||||||
|
>
|
||||||
|
<!-- 仪表盘 -->
|
||||||
|
<el-menu-item index="/admin/dashboard">
|
||||||
|
<el-icon><Monitor /></el-icon>
|
||||||
|
<span>仪表盘</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<!-- 用户管理 -->
|
||||||
|
<el-sub-menu index="users">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>用户管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/admin/users">用户列表</el-menu-item>
|
||||||
|
<el-menu-item v-if="authStore.isAdmin" index="/admin/warehouse/users">库存管理</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<!-- 订单管理 -->
|
||||||
|
<el-sub-menu index="orders" v-if="authStore.isAdmin">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>订单管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/admin/orders/purchase">买单管理</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/orders/sales">卖单管理</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<!-- 商品管理 (仅管理员) -->
|
||||||
|
<el-sub-menu v-if="authStore.isAdmin" index="products">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
<span>商品管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/admin/products/categories">商品分类</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/products/primary">一级商品</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/products/secondary">二级商品</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<!-- 积分管理 -->
|
||||||
|
<el-sub-menu index="scores">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><Star /></el-icon>
|
||||||
|
<span>积分管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/admin/scores">积分记录</el-menu-item>
|
||||||
|
<!-- <el-menu-item v-if="authStore.isAdmin" index="/admin/scores/create">添加积分</el-menu-item>-->
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<!-- 系统配置 (仅管理员) -->
|
||||||
|
<el-menu-item v-if="authStore.isAdmin" index="/admin/configs">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>系统配置</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<!-- 轮播图管理 (仅管理员) -->
|
||||||
|
<el-menu-item v-if="authStore.isAdmin" index="/admin/banners">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>轮播图管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<!-- 个人信息 -->
|
||||||
|
<el-menu-item index="/admin/profile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>个人信息</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<el-container>
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<el-header class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
class="collapse-btn"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<el-icon><Expand v-if="collapsed" /><Fold v-else /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-breadcrumb separator="/">
|
||||||
|
<el-breadcrumb-item
|
||||||
|
v-for="item in breadcrumbs"
|
||||||
|
:key="item.path"
|
||||||
|
:to="item.path"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<el-dropdown @command="handleCommand">
|
||||||
|
<span class="user-info">
|
||||||
|
<el-avatar :size="32" :src="authStore.user?.avatar">
|
||||||
|
<el-icon><UserFilled /></el-icon>
|
||||||
|
</el-avatar>
|
||||||
|
<span class="username">{{ authStore.user?.customer_name || authStore.user?.real_name }}</span>
|
||||||
|
<span class="user-role">{{ authStore.userRole }}</span>
|
||||||
|
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="profile">个人信息</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 主内容 -->
|
||||||
|
<el-main class="main-content">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade-transform" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const collapsed = ref(false)
|
||||||
|
|
||||||
|
// 切换侧边栏
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 面包屑导航
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
const matched = route.matched.filter(item => item.meta && item.meta.title)
|
||||||
|
const first = matched[0]
|
||||||
|
|
||||||
|
if (!first || first.name !== 'Admin') {
|
||||||
|
matched.unshift({ path: '/admin/dashboard', meta: { title: '首页' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return matched.map(item => ({
|
||||||
|
name: item.meta.title,
|
||||||
|
path: item.path
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理用户下拉菜单命令
|
||||||
|
const handleCommand = async (command) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'profile':
|
||||||
|
router.push('/admin/profile')
|
||||||
|
break
|
||||||
|
case 'logout':
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
authStore.logout()
|
||||||
|
ElMessage.success('退出登录成功')
|
||||||
|
router.push('/login')
|
||||||
|
} catch {
|
||||||
|
// 用户取消
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化,在小屏幕下自动收起侧边栏
|
||||||
|
watch(route, () => {
|
||||||
|
if (window.innerWidth < 992) {
|
||||||
|
collapsed.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-layout {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: #304156;
|
||||||
|
transition: width 0.28s;
|
||||||
|
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #2b3a4b;
|
||||||
|
border-bottom: 1px solid #1d2b3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
border: none;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu:not(.el-menu--collapse) {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
margin-right: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #5a5e66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-down {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面切换动画 */
|
||||||
|
.fade-transform-enter-active,
|
||||||
|
.fade-transform-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from "./router/index.js";
|
||||||
|
import ElementPlus, {ElMessage} from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import axios from 'axios'
|
||||||
|
import VueMarkdownEditor from '@kangc/v-md-editor';
|
||||||
|
import '@kangc/v-md-editor/lib/style/base-editor.css';
|
||||||
|
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
|
||||||
|
import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
|
||||||
|
import Prism from 'prismjs';
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import "./assets/main.css"
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
VueMarkdownEditor.use(vuepressTheme, {
|
||||||
|
Prism,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
const pinia = createPinia();
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
})
|
||||||
|
app.use(VueMarkdownEditor);
|
||||||
|
axios.defaults.baseURL = '/back';
|
||||||
|
axios.loadData = async function (url) {
|
||||||
|
const resp = await axios.get(url);
|
||||||
|
return resp.data;
|
||||||
|
};
|
||||||
|
axios.interceptors.request.use(function (config) {
|
||||||
|
|
||||||
|
if(localStorage.getItem("token"))
|
||||||
|
config.headers.token = localStorage.getItem("token");
|
||||||
|
return config;
|
||||||
|
}, function (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
// 响应拦截器
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
|
||||||
|
if(response.data.code==401){
|
||||||
|
ElMessage.error( "登陆异常")
|
||||||
|
router.push("/login")
|
||||||
|
response.status.code=401
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( response.data.success== false){
|
||||||
|
ElMessage.error( response.data.message)
|
||||||
|
}
|
||||||
|
// 对响应数据做点什么
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.config.globalProperties.$http = axios;
|
||||||
|
app.use(pinia);
|
||||||
|
app.use(router);
|
||||||
|
app.use(ElementPlus);
|
||||||
|
app.mount("#app");
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
// 导入页面组件
|
||||||
|
import Login from "@/views/Login.vue";
|
||||||
|
import AdminLayout from "@/layouts/AdminLayout.vue";
|
||||||
|
import Dashboard from "@/views/admin/Dashboard.vue";
|
||||||
|
import Users from "@/views/admin/Users.vue";
|
||||||
|
import PurchaseOrders from "@/views/admin/orders/PurchaseOrders.vue";
|
||||||
|
import SalesOrders from "@/views/admin/orders/SalesOrders.vue";
|
||||||
|
import Scores from "@/views/admin/Scores.vue";
|
||||||
|
import Categories from "@/views/admin/products/Categories.vue";
|
||||||
|
import PrimaryProducts from "@/views/admin/products/PrimaryProducts.vue";
|
||||||
|
import SecondaryProducts from "@/views/admin/products/SecondaryProducts.vue";
|
||||||
|
import SystemConfigs from "@/views/admin/SystemConfigs.vue";
|
||||||
|
import Banners from "@/views/admin/Banners.vue";
|
||||||
|
import Profile from "@/views/Profile.vue";
|
||||||
|
import UserWarehouses from "@/views/admin/UserWarehouses.vue";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
redirect: "/admin/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "Login",
|
||||||
|
component: Login,
|
||||||
|
meta: { title: "登录" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/admin",
|
||||||
|
name: "Admin",
|
||||||
|
component: AdminLayout,
|
||||||
|
redirect: "/admin/dashboard",
|
||||||
|
meta: {
|
||||||
|
title: "管理后台",
|
||||||
|
requiresAuth: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "dashboard",
|
||||||
|
name: "Dashboard",
|
||||||
|
component: Dashboard,
|
||||||
|
meta: { title: "仪表盘", requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "users",
|
||||||
|
name: "Users",
|
||||||
|
component: Users,
|
||||||
|
meta: { title: "用户管理", requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "warehouse/users",
|
||||||
|
name: "UserWarehouses",
|
||||||
|
component: UserWarehouses,
|
||||||
|
meta: { title: "用户库存管理", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "orders/purchase",
|
||||||
|
name: "PurchaseOrders",
|
||||||
|
component: PurchaseOrders,
|
||||||
|
meta: { title: "买单管理", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "orders/sales",
|
||||||
|
name: "SalesOrders",
|
||||||
|
component: SalesOrders,
|
||||||
|
meta: { title: "卖单管理", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "scores",
|
||||||
|
name: "Scores",
|
||||||
|
component: Scores,
|
||||||
|
meta: { title: "积分管理", requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "products/categories",
|
||||||
|
name: "ProductCategories",
|
||||||
|
component: Categories,
|
||||||
|
meta: { title: "商品分类", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "products/primary",
|
||||||
|
name: "PrimaryProducts",
|
||||||
|
component: PrimaryProducts,
|
||||||
|
meta: { title: "一级商品", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "products/secondary",
|
||||||
|
name: "SecondaryProducts",
|
||||||
|
component: SecondaryProducts,
|
||||||
|
meta: { title: "二级商品", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "configs",
|
||||||
|
name: "SystemConfigs",
|
||||||
|
component: SystemConfigs,
|
||||||
|
meta: { title: "系统配置", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "banners",
|
||||||
|
name: "Banners",
|
||||||
|
component: Banners,
|
||||||
|
meta: { title: "轮播图管理", requiresAuth: true, adminOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "profile",
|
||||||
|
name: "Profile",
|
||||||
|
component: Profile,
|
||||||
|
meta: { title: "个人信息", requiresAuth: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
// 检查是否需要登录
|
||||||
|
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||||
|
next("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已登录且访问登录页,重定向到管理后台
|
||||||
|
if (to.path === "/login" && authStore.isLoggedIn) {
|
||||||
|
next("/admin/dashboard");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if (to.meta.requiresAuth && authStore.isLoggedIn) {
|
||||||
|
// 检查是否为管理员或代理商
|
||||||
|
if (authStore.user?.system_role > 1) {
|
||||||
|
// 普通用户无权访问管理后台
|
||||||
|
next("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为管理员专用页面
|
||||||
|
if (to.meta.adminOnly && !authStore.isAdmin) {
|
||||||
|
// 非管理员无权访问管理员专用页面
|
||||||
|
next("/admin/dashboard");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
|
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
const isAdmin = computed(() => user.value?.system_role === 0)
|
||||||
|
const isAgent = computed(() => user.value?.system_role === 1)
|
||||||
|
const userRole = computed(() => {
|
||||||
|
if (!user.value) return ''
|
||||||
|
switch (user.value.system_role) {
|
||||||
|
case 0: return '管理员'
|
||||||
|
case 1: return '代理商'
|
||||||
|
case 2: return '普通用户'
|
||||||
|
default: return '未知'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const login = async (credentials) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/auth/login', credentials)
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const { token: newToken, user: userData } = response.data.data
|
||||||
|
|
||||||
|
token.value = newToken
|
||||||
|
user.value = userData
|
||||||
|
|
||||||
|
localStorage.setItem('token', newToken)
|
||||||
|
localStorage.setItem('user', JSON.stringify(userData))
|
||||||
|
|
||||||
|
return userData
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '登录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
const logout = () => {
|
||||||
|
token.value = ''
|
||||||
|
user.value = null
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
const getCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/auth/current')
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
user.value = response.data.data
|
||||||
|
localStorage.setItem('user', JSON.stringify(response.data.data))
|
||||||
|
return response.data.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
logout() // 如果获取用户信息失败,清除登录状态
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const hasPermission = (requiredRole = null) => {
|
||||||
|
if (!user.value) return false
|
||||||
|
|
||||||
|
// 管理员拥有所有权限
|
||||||
|
if (user.value.system_role === 0) return true
|
||||||
|
|
||||||
|
// 如果没有指定角色要求,只要登录即可
|
||||||
|
if (requiredRole === null) return true
|
||||||
|
|
||||||
|
// 检查特定角色权限
|
||||||
|
return user.value.system_role === requiredRole
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
const updateUserInfo = (userData) => {
|
||||||
|
user.value = userData
|
||||||
|
localStorage.setItem('user', JSON.stringify(userData))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
isLoggedIn,
|
||||||
|
isAdmin,
|
||||||
|
isAgent,
|
||||||
|
userRole,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
getCurrentUser,
|
||||||
|
hasPermission,
|
||||||
|
updateUserInfo
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1>管理员登录</h1>
|
||||||
|
<p>虚拟商品交易平台管理系统</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="loginFormRef"
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="loginRules"
|
||||||
|
class="login-form"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
>
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
prefix-icon="Phone"
|
||||||
|
size="large"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="login-button"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- <div class="login-footer">-->
|
||||||
|
<!-- <div class="demo-accounts">-->
|
||||||
|
<!-- <h4>测试账户:</h4>-->
|
||||||
|
<!-- <div class="account-item">-->
|
||||||
|
<!-- <span class="label">管理员:</span>-->
|
||||||
|
<!-- <el-button type="text" @click="fillAccount('admin')">13800138000 / admin123</el-button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="account-item">-->
|
||||||
|
<!-- <span class="label">代理商:</span>-->
|
||||||
|
<!-- <el-button type="text" @click="fillAccount('agent')">13800138001 / agent123</el-button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const loginFormRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
phone: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginRules = {
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充测试账户
|
||||||
|
const fillAccount = (type) => {
|
||||||
|
if (type === 'admin') {
|
||||||
|
loginForm.phone = '13800138000'
|
||||||
|
loginForm.password = 'admin123'
|
||||||
|
} else if (type === 'agent') {
|
||||||
|
loginForm.phone = '13800138001'
|
||||||
|
loginForm.password = 'agent123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await loginFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authStore.login(loginForm)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
if (authStore.isAdmin) {
|
||||||
|
router.push('/admin/dashboard')
|
||||||
|
}else {
|
||||||
|
router.push('/admin/users')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-accounts h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item .label {
|
||||||
|
color: #909399;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item .el-button {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item .el-button:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>个人信息</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-content">
|
||||||
|
<!-- 个人信息卡片 -->
|
||||||
|
<el-card class="profile-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>基本信息</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="profileFormRef"
|
||||||
|
:model="profileForm"
|
||||||
|
:rules="profileRules"
|
||||||
|
label-width="100px"
|
||||||
|
class="profile-form"
|
||||||
|
>
|
||||||
|
<!-- 头像上传 -->
|
||||||
|
<el-form-item label="头像">
|
||||||
|
<div class="avatar-upload">
|
||||||
|
<div class="avatar-container">
|
||||||
|
<el-image
|
||||||
|
v-if="profileForm.avatar"
|
||||||
|
:src="getImageUrl(profileForm.avatar)"
|
||||||
|
fit="cover"
|
||||||
|
class="avatar-preview"
|
||||||
|
/>
|
||||||
|
<div v-else class="avatar-placeholder">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ImageUpload
|
||||||
|
v-model="profileForm.avatar"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
class="avatar-uploader"
|
||||||
|
>
|
||||||
|
<el-button type="primary" size="small">
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
|
更换头像
|
||||||
|
</el-button>
|
||||||
|
</ImageUpload>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.phone"
|
||||||
|
disabled
|
||||||
|
placeholder="手机号码"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="客户姓名" prop="customer_name">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.customer_name"
|
||||||
|
placeholder="请输入客户姓名"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="真实姓名" prop="real_name">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.real_name"
|
||||||
|
placeholder="请输入真实姓名"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="公司全称" prop="company_name">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.company_name"
|
||||||
|
placeholder="请输入公司全称"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="个人介绍" prop="personal_intro">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.personal_intro"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入个人介绍"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="身份码">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.identity_code"
|
||||||
|
disabled
|
||||||
|
placeholder="系统自动生成"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="推荐人身份码">
|
||||||
|
<el-input
|
||||||
|
v-model="profileForm.referrer_identity_code"
|
||||||
|
disabled
|
||||||
|
placeholder="推荐人身份码"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 证件图片 -->
|
||||||
|
<el-form-item label="营业执照">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="profileForm.business_license"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="身份证正面">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="profileForm.id_card_front"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="身份证反面">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="profileForm.id_card_back"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 收款码 -->
|
||||||
|
<el-form-item label="主收款码">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="profileForm.main_payment_code"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="副收款码">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="profileForm.backup_payment_code"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="updateProfile">
|
||||||
|
保存修改
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 修改密码卡片 -->
|
||||||
|
<el-card class="password-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>修改密码</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="passwordFormRef"
|
||||||
|
:model="passwordForm"
|
||||||
|
:rules="passwordRules"
|
||||||
|
label-width="100px"
|
||||||
|
class="password-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="当前密码" prop="current_password">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.current_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入当前密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="新密码" prop="new_password">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.new_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认密码" prop="confirm_password">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.confirm_password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请确认新密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="passwordSubmitting" @click="updatePassword">
|
||||||
|
修改密码
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetPasswordForm">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { User, Upload } from '@element-plus/icons-vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import ImageUpload from '@/components/ImageUpload.vue'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const submitting = ref(false)
|
||||||
|
const passwordSubmitting = ref(false)
|
||||||
|
const profileFormRef = ref()
|
||||||
|
const passwordFormRef = ref()
|
||||||
|
|
||||||
|
// 个人信息表单
|
||||||
|
const profileForm = reactive({
|
||||||
|
phone: '',
|
||||||
|
customer_name: '',
|
||||||
|
real_name: '',
|
||||||
|
avatar: '',
|
||||||
|
company_name: '',
|
||||||
|
personal_intro: '',
|
||||||
|
identity_code: '',
|
||||||
|
referrer_identity_code: '',
|
||||||
|
business_license: '',
|
||||||
|
id_card_front: '',
|
||||||
|
id_card_back: '',
|
||||||
|
main_payment_code: '',
|
||||||
|
backup_payment_code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 密码修改表单
|
||||||
|
const passwordForm = reactive({
|
||||||
|
current_password: '',
|
||||||
|
new_password: '',
|
||||||
|
confirm_password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 个人信息验证规则
|
||||||
|
const profileRules = {
|
||||||
|
customer_name: [
|
||||||
|
{ required: true, message: '请输入客户姓名', trigger: 'blur' },
|
||||||
|
{ max: 50, message: '客户姓名不能超过50个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
real_name: [
|
||||||
|
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||||
|
{ max: 50, message: '真实姓名不能超过50个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
company_name: [
|
||||||
|
{ max: 100, message: '公司名称不能超过100个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
personal_intro: [
|
||||||
|
{ max: 500, message: '个人介绍不能超过500个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码验证规则
|
||||||
|
const passwordRules = {
|
||||||
|
current_password: [
|
||||||
|
{ required: true, message: '请输入当前密码', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
new_password: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6个字符', trigger: 'blur' },
|
||||||
|
{ max: 20, message: '密码长度不能超过20个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirm_password: [
|
||||||
|
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value !== passwordForm.new_password) {
|
||||||
|
callback(new Error('两次输入密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
const getCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/current-user')
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const userData = response.data.data
|
||||||
|
Object.assign(profileForm, {
|
||||||
|
phone: userData.phone || '',
|
||||||
|
customer_name: userData.customer_name || '',
|
||||||
|
real_name: userData.real_name || '',
|
||||||
|
avatar: userData.avatar || '',
|
||||||
|
company_name: userData.company_name || '',
|
||||||
|
personal_intro: userData.personal_intro || '',
|
||||||
|
identity_code: userData.identity_code || '',
|
||||||
|
referrer_identity_code: userData.referrer_identity_code || '',
|
||||||
|
business_license: userData.business_license_image || '',
|
||||||
|
id_card_front: userData.id_card_front_image || '',
|
||||||
|
id_card_back: userData.id_card_back_image || '',
|
||||||
|
main_payment_code: userData.main_payment_qr_image || '',
|
||||||
|
backup_payment_code: userData.sub_payment_qr_image || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
ElMessage.error('获取用户信息失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${window.location.origin}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新个人信息
|
||||||
|
const updateProfile = async () => {
|
||||||
|
if (!profileFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await profileFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.put('/update-profile', {
|
||||||
|
customer_name: profileForm.customer_name,
|
||||||
|
real_name: profileForm.real_name,
|
||||||
|
avatar: profileForm.avatar,
|
||||||
|
company_name: profileForm.company_name,
|
||||||
|
personal_intro: profileForm.personal_intro,
|
||||||
|
business_license: profileForm.business_license,
|
||||||
|
id_card_front: profileForm.id_card_front,
|
||||||
|
id_card_back: profileForm.id_card_back,
|
||||||
|
main_payment_code: profileForm.main_payment_code,
|
||||||
|
backup_payment_code: profileForm.backup_payment_code
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
ElMessage.success('个人信息更新成功')
|
||||||
|
// 更新全局用户信息
|
||||||
|
authStore.updateUserInfo(response.data.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新个人信息失败:', error)
|
||||||
|
ElMessage.error('更新个人信息失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
const updatePassword = async () => {
|
||||||
|
if (!passwordFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await passwordFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
passwordSubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.put('/change-password', {
|
||||||
|
current_password: passwordForm.current_password,
|
||||||
|
new_password: passwordForm.new_password
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
ElMessage.success('密码修改成功')
|
||||||
|
resetPasswordForm()
|
||||||
|
|
||||||
|
// 提醒用户重新登录
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'密码已修改成功,为了安全起见,请重新登录。',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '重新登录',
|
||||||
|
cancelButtonText: '稍后再说',
|
||||||
|
type: 'success'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
authStore.logout()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败:', error)
|
||||||
|
const message = error.response?.data?.message || '修改密码失败'
|
||||||
|
ElMessage.error(message)
|
||||||
|
} finally {
|
||||||
|
passwordSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码表单
|
||||||
|
const resetPasswordForm = () => {
|
||||||
|
Object.assign(passwordForm, {
|
||||||
|
current_password: '',
|
||||||
|
new_password: '',
|
||||||
|
confirm_password: ''
|
||||||
|
})
|
||||||
|
if (passwordFormRef.value) {
|
||||||
|
passwordFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取用户信息
|
||||||
|
onMounted(() => {
|
||||||
|
getCurrentUser()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card,
|
||||||
|
.password-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-form,
|
||||||
|
.password-form {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-upload {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px dashed #dcdfe6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-form,
|
||||||
|
.password-form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-upload {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
<template>
|
||||||
|
<div class="banners-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>轮播图管理</h2>
|
||||||
|
<el-button type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加轮播图
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 轮播图列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="banners"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column label="图片" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
v-if="row.image_url"
|
||||||
|
:src="(row.image_url)"
|
||||||
|
|
||||||
|
class="banner-preview"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
<span v-else class="no-image">无图片</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- <el-table-column prop="link_url" label="链接地址" min-width="200" show-overflow-tooltip />-->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />-->
|
||||||
|
<!-- -->
|
||||||
|
<el-table-column prop="sort_order" label="排序" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="row.status === 1 ? 'success' : 'danger'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="danger-text"
|
||||||
|
@click="deleteBanner(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getBanners"
|
||||||
|
@current-change="getBanners"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑轮播图对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogMode === 'create' ? '添加轮播图' : '编辑轮播图'"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="bannerFormRef"
|
||||||
|
:model="bannerForm"
|
||||||
|
:rules="bannerRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="bannerForm.title"
|
||||||
|
placeholder="请输入轮播图标题"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="图片" prop="image_url">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="bannerForm.image_url"
|
||||||
|
:limit="1"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- <el-form-item label="链接地址" prop="link_url">-->
|
||||||
|
<!-- <el-input-->
|
||||||
|
<!-- v-model="bannerForm.link_url"-->
|
||||||
|
<!-- placeholder="请输入跳转链接(可选)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </el-form-item>-->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <el-form-item label="描述" prop="description">-->
|
||||||
|
<!-- <el-input-->
|
||||||
|
<!-- v-model="bannerForm.description"-->
|
||||||
|
<!-- type="textarea"-->
|
||||||
|
<!-- :rows="3"-->
|
||||||
|
<!-- placeholder="请输入描述信息(可选)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </el-form-item>-->
|
||||||
|
|
||||||
|
<el-form-item label="排序" prop="sort_order">
|
||||||
|
<el-input-number
|
||||||
|
v-model="bannerForm.sort_order"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
placeholder="数字越小越靠前"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="bannerForm.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="禁用"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitLoading" @click="submitBanner">
|
||||||
|
{{ dialogMode === 'create' ? '添加' : '保存' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import ImageUpload from '@/components/ImageUpload.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const banners = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogMode = ref('create') // create | edit
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 轮播图表单
|
||||||
|
const bannerForm = reactive({
|
||||||
|
id: null,
|
||||||
|
title: '',
|
||||||
|
image_url: '',
|
||||||
|
link_url: '',
|
||||||
|
description: '',
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const bannerFormRef = ref()
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const bannerRules = {
|
||||||
|
title: [
|
||||||
|
{ required: true, message: '请输入轮播图标题', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
image_url: [
|
||||||
|
{ required: true, message: '请上传轮播图图片', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取轮播图列表
|
||||||
|
const getBanners = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get('/admin/banners', {
|
||||||
|
params: {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
banners.value = data.data.list || []
|
||||||
|
pagination.total = data.data.total || 0
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data.message || '获取轮播图列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取轮播图列表失败:', error)
|
||||||
|
ElMessage.error('获取轮播图列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create'
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (banner) => {
|
||||||
|
dialogMode.value = 'edit'
|
||||||
|
Object.assign(bannerForm, {
|
||||||
|
id: banner.id,
|
||||||
|
title: banner.title,
|
||||||
|
image_url: banner.image_url,
|
||||||
|
link_url: banner.link_url || '',
|
||||||
|
description: banner.description || '',
|
||||||
|
sort_order: banner.sort_order || 0,
|
||||||
|
status: banner.status
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(bannerForm, {
|
||||||
|
id: null,
|
||||||
|
title: '',
|
||||||
|
image_url: '',
|
||||||
|
link_url: '',
|
||||||
|
description: '',
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
bannerFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交轮播图
|
||||||
|
const submitBanner = async () => {
|
||||||
|
if (!bannerFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await bannerFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const url = dialogMode.value === 'create'
|
||||||
|
? '/admin/banners'
|
||||||
|
: `/admin/banners/${bannerForm.id}`
|
||||||
|
|
||||||
|
const method = dialogMode.value === 'create' ? 'post' : 'put'
|
||||||
|
|
||||||
|
const { data } = await axios[method](url, {
|
||||||
|
title: bannerForm.title,
|
||||||
|
image_url: bannerForm.image_url,
|
||||||
|
link_url: bannerForm.link_url,
|
||||||
|
description: bannerForm.description,
|
||||||
|
sort_order: bannerForm.sort_order,
|
||||||
|
status: bannerForm.status
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
ElMessage.success(dialogMode.value === 'create' ? '轮播图添加成功' : '轮播图更新成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
getBanners()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data.message || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交轮播图失败:', error)
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除轮播图
|
||||||
|
const deleteBanner = async (banner) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除轮播图 "${banner.title}" 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data } = await axios.delete(`/admin/banners/${banner.id}`)
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
ElMessage.success('轮播图删除成功')
|
||||||
|
getBanners()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除轮播图失败:', error)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完整图片URL
|
||||||
|
const getFullImageUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${axios.defaults.baseURL}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
return new Date(date).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载
|
||||||
|
onMounted(() => {
|
||||||
|
getBanners()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.banners-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-preview {
|
||||||
|
width: 80px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text:hover {
|
||||||
|
color: #f56c6c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h2>仪表盘</h2>
|
||||||
|
<p>欢迎回来,{{ authStore.user?.customer_name || authStore.user?.real_name }}!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据统计卡片 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon user-icon">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-number">{{ stats.totalUsers || 0 }}</div>
|
||||||
|
<div class="stat-label">总用户数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon order-icon">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
|
||||||
|
<div class="stat-label">总订单数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon product-icon">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-number">{{ stats.totalProducts || 0 }}</div>
|
||||||
|
<div class="stat-label">商品总数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon money-icon">
|
||||||
|
<el-icon><Money /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-number">¥{{ formatAmount(stats.totalAmount) }}</div>
|
||||||
|
<div class="stat-label">交易总额</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户搜索统计 -->
|
||||||
|
<div class="user-search-section">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>用户统计查询</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="search-form">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-input
|
||||||
|
v-model="userSearchQuery"
|
||||||
|
placeholder="输入手机号或用户姓名"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="searchUserStats"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button type="primary" @click="searchUserStats" :loading="userSearchLoading">
|
||||||
|
查询统计
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户统计结果 -->
|
||||||
|
<div v-if="selectedUserStats" class="user-stats-result">
|
||||||
|
<el-divider content-position="left">用户信息</el-divider>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item label="客户姓名">{{ selectedUserStats.user.customer_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="真实姓名">{{ selectedUserStats.user.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">{{ selectedUserStats.user.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户角色">{{ getUserRoleName(selectedUserStats.user.system_role) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="注册时间">{{ formatTime(selectedUserStats.user.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司名称">{{ selectedUserStats.user.company_name || '未填写' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-divider content-position="left">
|
||||||
|
统计数据
|
||||||
|
<span v-if="dateRange && dateRange.length === 2" style="font-size: 12px; color: #909399; margin-left: 10px;">
|
||||||
|
({{ dateRange[0] }} 至 {{ dateRange[1] }})
|
||||||
|
</span>
|
||||||
|
</el-divider>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="采购订单数" :value="selectedUserStats.stats.purchase_orders_count || 0" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="销售订单数" :value="selectedUserStats.stats.sales_orders_count || 0" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="采购总金额" :value="selectedUserStats.stats.total_purchase_amount || 0" prefix="¥" :precision="2" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="销售总金额" :value="selectedUserStats.stats.total_sales_amount || 0" prefix="¥" :precision="2" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="库存商品种类" :value="selectedUserStats.stats.warehouse_items_count || 0" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="库存商品总数" :value="selectedUserStats.stats.total_warehouse_quantity || 0" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="积分余额" :value="selectedUserStats.stats.score_balance || 0" />
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="6">-->
|
||||||
|
<!-- <el-statistic title="最后活动时间" :value="formatTime(selectedUserStats.stats.last_activity_time)" />-->
|
||||||
|
<!-- </el-col>-->
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="userSearched && !selectedUserStats" class="no-user-found">
|
||||||
|
<el-empty description="未找到匹配的用户" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="charts-section" v-if="authStore.isAdmin">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card title="用户角色分布">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>用户角色分布</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="roleChartRef" class="chart-container"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card title="订单趋势">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近7天订单趋势</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="orderTrendChartRef" class="chart-container"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近活动 -->
|
||||||
|
<div v-if="authStore.isAdmin" class="recent-activities">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近活动</span>
|
||||||
|
<el-button type="text" @click="refreshActivities">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-timeline>
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="activity in recentActivities"
|
||||||
|
:key="activity.id"
|
||||||
|
:timestamp="activity.time"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-card>
|
||||||
|
<p>{{ activity.description }}</p>
|
||||||
|
</el-card>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
|
||||||
|
<div v-if="recentActivities.length === 0" class="no-data">
|
||||||
|
<el-empty description="暂无活动记录" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div >
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const stats = ref({})
|
||||||
|
const recentActivities = ref([])
|
||||||
|
const roleChartRef = ref(null)
|
||||||
|
const orderTrendChartRef = ref(null)
|
||||||
|
const userInfo=ref( null)
|
||||||
|
// 用户搜索相关
|
||||||
|
const userSearchQuery = ref('')
|
||||||
|
const dateRange = ref([])
|
||||||
|
const selectedUserStats = ref(null)
|
||||||
|
const userSearchLoading = ref(false)
|
||||||
|
const userSearched = ref(false)
|
||||||
|
|
||||||
|
let roleChart = null
|
||||||
|
let orderTrendChart = null
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
const getStats = async () => {
|
||||||
|
try {
|
||||||
|
const [userStats, orderStats, productStats, orderTrendData] = await Promise.all([
|
||||||
|
axios.get('/admin/users/stats'),
|
||||||
|
axios.get('/admin/orders/purchase/stats'),
|
||||||
|
authStore.isAdmin ? axios.get('/admin/products/primary/stats') : Promise.resolve({ data: { data: {} } }),
|
||||||
|
axios.get('/admin/orders/purchase/trend')
|
||||||
|
])
|
||||||
|
|
||||||
|
// 处理订单趋势数据
|
||||||
|
const trendData = orderTrendData.data.data.trend || []
|
||||||
|
const days = trendData.map(item => item.date)
|
||||||
|
const orders = trendData.map(item => item.count)
|
||||||
|
|
||||||
|
stats.value = {
|
||||||
|
totalUsers: userStats.data.data.total_users || 0,
|
||||||
|
totalOrders: (orderStats.data.data.total_purchase_orders || 0) + (orderStats.data.data.total_sales_orders || 0),
|
||||||
|
totalProducts: productStats.data.data.total_primary_products || 0,
|
||||||
|
totalAmount: (orderStats.data.data.total_purchase_amount || 0) + (orderStats.data.data.total_sales_amount || 0),
|
||||||
|
roleStats: userStats.data.data.role_counts || [],
|
||||||
|
orderTrend: { days, orders }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化角色分布图表
|
||||||
|
const initRoleChart = () => {
|
||||||
|
if (!roleChartRef.value) return
|
||||||
|
|
||||||
|
roleChart = echarts.init(roleChartRef.value)
|
||||||
|
|
||||||
|
const roleNames = { 0: '管理员', 1: '代理商', 2: '普通用户' }
|
||||||
|
const data = (stats.value.roleStats || []).map(item => ({
|
||||||
|
name: roleNames[item.system_role] || '未知',
|
||||||
|
value: item.count
|
||||||
|
}))
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
bottom: 10,
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '用户角色',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '18',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
roleChart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化订单趋势图表
|
||||||
|
const initOrderTrendChart = () => {
|
||||||
|
if (!orderTrendChartRef.value) return
|
||||||
|
|
||||||
|
orderTrendChart = echarts.init(orderTrendChartRef.value)
|
||||||
|
|
||||||
|
const trendData = stats.value.orderTrend || { days: [], orders: [] }
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: trendData.days
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '订单数',
|
||||||
|
type: 'line',
|
||||||
|
stack: 'Total',
|
||||||
|
areaStyle: {},
|
||||||
|
data: trendData.orders
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
orderTrendChart.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近活动
|
||||||
|
const getRecentActivities = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/admin/orders/purchase/activities')
|
||||||
|
if (response.data.success) {
|
||||||
|
// 格式化时间
|
||||||
|
recentActivities.value = (response.data.data.activities || []).map(activity => ({
|
||||||
|
id: activity.id,
|
||||||
|
description: activity.description,
|
||||||
|
time: new Date(activity.time).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取最近活动失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新活动记录
|
||||||
|
const refreshActivities = () => {
|
||||||
|
getRecentActivities()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色名称
|
||||||
|
const getUserRoleName = (role) => {
|
||||||
|
const roleNames = { 0: '管理员', 1: '代理商', 2: '普通用户' }
|
||||||
|
return roleNames[role] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timeStr) => {
|
||||||
|
if (!timeStr) return '未知'
|
||||||
|
return new Date(timeStr).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索用户统计
|
||||||
|
const searchUserStats = async () => {
|
||||||
|
if (!userSearchQuery.value.trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSearchLoading.value = true
|
||||||
|
userSearched.value = true
|
||||||
|
selectedUserStats.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
query: userSearchQuery.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加时间范围参数
|
||||||
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
|
params.start_date = dateRange.value[0]
|
||||||
|
params.end_date = dateRange.value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/users/search-stats', {
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
selectedUserStats.value = response.data.data
|
||||||
|
} else {
|
||||||
|
selectedUserStats.value = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索用户统计失败:', error)
|
||||||
|
selectedUserStats.value = null
|
||||||
|
} finally {
|
||||||
|
userSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
await getStats()
|
||||||
|
await getRecentActivities()
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
initRoleChart()
|
||||||
|
initOrderTrendChart()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应式图表
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (roleChart) roleChart.resize()
|
||||||
|
if (orderTrendChart) orderTrendChart.resize()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h2 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-icon {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-icon {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-icon {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-activities {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats-result {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-user-found {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-section .el-col {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scores-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>积分管理</h2>
|
||||||
|
<el-button v-if="authStore.isAdmin" type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加积分记录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="用户ID">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.user_id"
|
||||||
|
placeholder="请输入用户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="变化类型">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.change_type"
|
||||||
|
placeholder="请选择类型"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="增加" value="positive" />
|
||||||
|
<el-option label="减少" value="negative" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getScoreRecords">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 积分记录列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="scoreRecords"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||||
|
|
||||||
|
<el-table-column label="用户信息" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.user?.customer_name || row.user?.phone }}</div>
|
||||||
|
<div class="text-xs text-gray-500">ID: {{ row.user_id }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ row.user?.phone }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="change_number" label="积分变化" width="120" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getChangeNumberClass(row.change_number)">
|
||||||
|
{{ row.change_number > 0 ? '+' : '' }}{{ row.change_number }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="当前积分" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-semibold">{{ row.user?.current_points || 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="note" label="说明" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showScoreDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<!-- <el-button-->
|
||||||
|
<!-- v-if="authStore.isAdmin"-->
|
||||||
|
<!-- type="text"-->
|
||||||
|
<!-- size="small"-->
|
||||||
|
<!-- @click="showEditDialog(row)"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- 编辑-->
|
||||||
|
<!-- </el-button>-->
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="danger-text"
|
||||||
|
@click="deleteScoreRecord(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getScoreRecords"
|
||||||
|
@current-change="getScoreRecords"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑积分记录对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogMode === 'create' ? '添加积分记录' : '编辑积分记录'"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="scoreFormRef"
|
||||||
|
:model="scoreForm"
|
||||||
|
:rules="scoreRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户" prop="user_id">
|
||||||
|
<el-select
|
||||||
|
v-model="scoreForm.user_id"
|
||||||
|
placeholder="请选择用户"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
:remote-method="searchUsers"
|
||||||
|
:loading="userSearchLoading"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="user in userOptions"
|
||||||
|
:key="user.id"
|
||||||
|
:label="`${user.customer_name || user.real_name} (${user.phone})`"
|
||||||
|
:value="user.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="积分变化" prop="change_number">
|
||||||
|
<el-input-number
|
||||||
|
v-model="scoreForm.change_number"
|
||||||
|
:min="-999999"
|
||||||
|
:max="999999"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="说明" prop="note">
|
||||||
|
<el-input
|
||||||
|
v-model="scoreForm.note"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入变化说明"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitScore">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 积分记录详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="积分记录详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="记录ID">{{ currentRecord.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户ID">{{ currentRecord.user_id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户姓名">{{ currentRecord.user?.customer_name || currentRecord.user?.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户手机">{{ currentRecord.user?.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="积分变化">
|
||||||
|
<span :class="getChangeNumberClass(currentRecord.change_number)">
|
||||||
|
{{ currentRecord.change_number > 0 ? '+' : '' }}{{ currentRecord.change_number }}
|
||||||
|
</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前积分">{{ currentRecord.user?.current_points || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="说明" :span="2">{{ currentRecord.note }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentRecord.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDate(currentRecord.updated_at) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const scoreRecords = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const dialogMode = ref('create')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const scoreFormRef = ref()
|
||||||
|
const userSearchLoading = ref(false)
|
||||||
|
const userOptions = ref([])
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
user_id: '',
|
||||||
|
phone: '',
|
||||||
|
change_type: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 积分记录表单
|
||||||
|
const scoreForm = reactive({
|
||||||
|
user_id: '',
|
||||||
|
change_number: 0,
|
||||||
|
note: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的记录
|
||||||
|
const currentRecord = ref({})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const scoreRules = {
|
||||||
|
user_id: [
|
||||||
|
{ required: true, message: '请选择用户', trigger: 'change' }
|
||||||
|
],
|
||||||
|
change_number: [
|
||||||
|
{ required: true, message: '请输入积分变化', trigger: 'blur' },
|
||||||
|
{ type: 'number', message: '积分变化必须为数字' }
|
||||||
|
],
|
||||||
|
note: [
|
||||||
|
{ required: true, message: '请输入变化说明', trigger: 'blur' },
|
||||||
|
{ max: 200, message: '说明不能超过200个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取积分记录列表
|
||||||
|
const getScoreRecords = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/scores', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
scoreRecords.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取积分记录失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
user_id: '',
|
||||||
|
phone: '',
|
||||||
|
change_type: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getScoreRecords()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理排序
|
||||||
|
const handleSortChange = ({ prop, order }) => {
|
||||||
|
// 实现排序逻辑
|
||||||
|
console.log('排序:', prop, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取积分变化样式
|
||||||
|
const getChangeNumberClass = (changeNumber) => {
|
||||||
|
if (changeNumber > 0) {
|
||||||
|
return 'text-green-500 font-semibold'
|
||||||
|
} else if (changeNumber < 0) {
|
||||||
|
return 'text-red-500 font-semibold'
|
||||||
|
}
|
||||||
|
return 'text-gray-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索用户
|
||||||
|
const searchUsers = async (query) => {
|
||||||
|
if (!query) {
|
||||||
|
userOptions.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSearchLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/admin/users', {
|
||||||
|
params: {
|
||||||
|
phone: query,
|
||||||
|
page: 1,
|
||||||
|
page_size: 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
userOptions.value = response.data.data.list || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索用户失败:', error)
|
||||||
|
} finally {
|
||||||
|
userSearchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create'
|
||||||
|
resetScoreForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (record) => {
|
||||||
|
dialogMode.value = 'edit'
|
||||||
|
Object.assign(scoreForm, {
|
||||||
|
user_id: record.user_id,
|
||||||
|
change_number: record.change_number,
|
||||||
|
note: record.note
|
||||||
|
})
|
||||||
|
// 预填充用户选项
|
||||||
|
userOptions.value = [record.user]
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示积分记录详情
|
||||||
|
const showScoreDetail = (record) => {
|
||||||
|
currentRecord.value = record
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置积分记录表单
|
||||||
|
const resetScoreForm = () => {
|
||||||
|
Object.assign(scoreForm, {
|
||||||
|
user_id: '',
|
||||||
|
change_number: 0,
|
||||||
|
note: ''
|
||||||
|
})
|
||||||
|
userOptions.value = []
|
||||||
|
if (scoreFormRef.value) {
|
||||||
|
scoreFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交积分记录表单
|
||||||
|
const submitScore = async () => {
|
||||||
|
if (!scoreFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await scoreFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (dialogMode.value === 'create') {
|
||||||
|
await axios.post('/admin/scores', scoreForm)
|
||||||
|
ElMessage.success('积分记录创建成功')
|
||||||
|
} else {
|
||||||
|
await axios.put(`/admin/scores/${currentRecord.value.id}`, scoreForm)
|
||||||
|
ElMessage.success('积分记录更新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = false
|
||||||
|
getScoreRecords()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交积分记录失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除积分记录
|
||||||
|
const deleteScoreRecord = async (record) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除这条积分记录吗?删除后用户积分将会恢复。`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await axios.delete(`/admin/scores/${record.id}`)
|
||||||
|
ElMessage.success('积分记录删除成功')
|
||||||
|
getScoreRecords()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除积分记录失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getScoreRecords()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scores-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-green-500 {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text:hover {
|
||||||
|
color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
<template>
|
||||||
|
<div class="system-configs-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>系统配置管理</h2>
|
||||||
|
<el-button type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="配置键">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.config_key"
|
||||||
|
placeholder="请输入配置键"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置类型">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.config_type"
|
||||||
|
placeholder="请选择类型"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="字符串" value="string" />
|
||||||
|
<el-option label="数字" value="number" />
|
||||||
|
<el-option label="布尔值" value="boolean" />
|
||||||
|
<el-option label="JSON" value="json" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="系统配置">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.is_system"
|
||||||
|
placeholder="请选择"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
|
<el-option label="是" value="1" />
|
||||||
|
<el-option label="否" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getConfigs">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 配置列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="configs"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="config_key" label="配置键" width="200" />
|
||||||
|
|
||||||
|
<el-table-column prop="config_name" label="配置名称" width="150" />
|
||||||
|
|
||||||
|
<el-table-column prop="config_value" label="配置值" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="config-value">
|
||||||
|
<span v-if="row.config_type === 'boolean'">
|
||||||
|
<el-tag :type="row.config_value === 'true' ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.config_value === 'true' ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="row.config_type === 'json'" class="json-value">
|
||||||
|
{{ formatJsonValue(row.config_value) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ row.config_value }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="config_type" label="类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getTypeTagColor(row.config_type)" size="small">
|
||||||
|
{{ getTypeName(row.config_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="is_system" label="系统配置" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_system === 1 ? 'warning' : 'info'" size="small">
|
||||||
|
{{ row.is_system === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="config_description" label="描述" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showConfigDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.is_system !== 1"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="danger-text"
|
||||||
|
@click="deleteConfig(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getConfigs"
|
||||||
|
@current-change="getConfigs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑配置对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogMode === 'create' ? '添加配置' : '编辑配置'"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="configFormRef"
|
||||||
|
:model="configForm"
|
||||||
|
:rules="configRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="配置键" prop="config_key">
|
||||||
|
<el-input
|
||||||
|
v-model="configForm.config_key"
|
||||||
|
placeholder="请输入配置键(英文字母、数字、下划线)"
|
||||||
|
:disabled="dialogMode === 'edit' && configForm.is_system === 1"
|
||||||
|
maxlength="100"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置名称" prop="config_name">
|
||||||
|
<el-input
|
||||||
|
v-model="configForm.config_name"
|
||||||
|
placeholder="请输入配置名称"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置类型" prop="config_type">
|
||||||
|
<el-select
|
||||||
|
v-model="configForm.config_type"
|
||||||
|
placeholder="请选择配置类型"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="onTypeChange"
|
||||||
|
>
|
||||||
|
<el-option label="字符串" value="string" />
|
||||||
|
<el-option label="数字" value="number" />
|
||||||
|
<el-option label="布尔值" value="boolean" />
|
||||||
|
<el-option label="JSON" value="json" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置值" prop="config_value">
|
||||||
|
<!-- 字符串类型 -->
|
||||||
|
<el-input
|
||||||
|
v-if="configForm.config_type === 'string'"
|
||||||
|
v-model="configForm.config_value"
|
||||||
|
placeholder="请输入配置值"
|
||||||
|
maxlength="1000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 数字类型 -->
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="configForm.config_type === 'number'"
|
||||||
|
v-model="configForm.config_value"
|
||||||
|
:precision="6"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 布尔值类型 -->
|
||||||
|
<el-radio-group
|
||||||
|
v-else-if="configForm.config_type === 'boolean'"
|
||||||
|
v-model="configForm.config_value"
|
||||||
|
>
|
||||||
|
<el-radio label="true">是</el-radio>
|
||||||
|
<el-radio label="false">否</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
|
||||||
|
<!-- JSON类型 -->
|
||||||
|
<el-input
|
||||||
|
v-else-if="configForm.config_type === 'json'"
|
||||||
|
v-model="configForm.config_value"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="请输入有效的JSON格式"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 默认 -->
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="configForm.config_value"
|
||||||
|
placeholder="请输入配置值"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置描述">
|
||||||
|
<el-input
|
||||||
|
v-model="configForm.config_description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入配置描述"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitConfig">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 配置详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="配置详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="配置ID">{{ currentConfig.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="配置键">{{ currentConfig.config_key }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="配置名称">{{ currentConfig.config_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="配置类型">
|
||||||
|
<el-tag :type="getTypeTagColor(currentConfig.config_type)">
|
||||||
|
{{ getTypeName(currentConfig.config_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="系统配置">
|
||||||
|
<el-tag :type="currentConfig.is_system === 1 ? 'warning' : 'info'">
|
||||||
|
{{ currentConfig.is_system === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="配置值">
|
||||||
|
<div class="config-value-detail">
|
||||||
|
<span v-if="currentConfig.config_type === 'boolean'">
|
||||||
|
<el-tag :type="currentConfig.config_value === 'true' ? 'success' : 'danger'">
|
||||||
|
{{ currentConfig.config_value === 'true' ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<pre v-else-if="currentConfig.config_type === 'json'" class="json-preview">{{ formatJson(currentConfig.config_value) }}</pre>
|
||||||
|
<span v-else>{{ currentConfig.config_value }}</span>
|
||||||
|
</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="配置描述">{{ currentConfig.config_description || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentConfig.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDate(currentConfig.updated_at) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const configs = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const dialogMode = ref('create')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const configFormRef = ref()
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
config_key: '',
|
||||||
|
config_type: '',
|
||||||
|
is_system: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 配置表单
|
||||||
|
const configForm = reactive({
|
||||||
|
config_key: '',
|
||||||
|
config_name: '',
|
||||||
|
config_value: '',
|
||||||
|
config_type: 'string',
|
||||||
|
config_description: '',
|
||||||
|
is_system: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的配置
|
||||||
|
const currentConfig = ref({})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const configRules = {
|
||||||
|
config_key: [
|
||||||
|
{ required: true, message: '请输入配置键', trigger: 'blur' },
|
||||||
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '配置键只能包含字母、数字和下划线', trigger: 'blur' },
|
||||||
|
{ max: 100, message: '配置键不能超过100个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
config_name: [
|
||||||
|
{ required: true, message: '请输入配置名称', trigger: 'blur' },
|
||||||
|
{ max: 200, message: '配置名称不能超过200个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
config_type: [
|
||||||
|
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
config_value: [
|
||||||
|
{ required: true, message: '请输入配置值', trigger: 'blur' },
|
||||||
|
{ validator: validateConfigValue, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置值验证器
|
||||||
|
function validateConfigValue(rule, value, callback) {
|
||||||
|
if (!value && value !== 0 && value !== false) {
|
||||||
|
callback(new Error('请输入配置值'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = configForm.config_type
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
if (isNaN(Number(value))) {
|
||||||
|
callback(new Error('请输入有效的数字'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (type === 'boolean') {
|
||||||
|
if (value !== 'true' && value !== 'false') {
|
||||||
|
callback(new Error('请选择布尔值'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (type === 'json') {
|
||||||
|
try {
|
||||||
|
JSON.parse(value)
|
||||||
|
} catch (error) {
|
||||||
|
callback(new Error('请输入有效的JSON格式'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置列表
|
||||||
|
const getConfigs = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/configs', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
configs.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
config_key: '',
|
||||||
|
config_type: '',
|
||||||
|
is_system: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取类型名称
|
||||||
|
const getTypeName = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
string: '字符串',
|
||||||
|
number: '数字',
|
||||||
|
boolean: '布尔值',
|
||||||
|
json: 'JSON'
|
||||||
|
}
|
||||||
|
return typeMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取类型标签颜色
|
||||||
|
const getTypeTagColor = (type) => {
|
||||||
|
const colorMap = {
|
||||||
|
string: '',
|
||||||
|
number: 'success',
|
||||||
|
boolean: 'warning',
|
||||||
|
json: 'danger'
|
||||||
|
}
|
||||||
|
return colorMap[type] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化JSON值(简化显示)
|
||||||
|
const formatJsonValue = (value) => {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(value)
|
||||||
|
return typeof obj === 'object' ? `{${Object.keys(obj).length} 项}` : value
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化JSON(详细显示)
|
||||||
|
const formatJson = (value) => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(value), null, 2)
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型变化处理
|
||||||
|
const onTypeChange = (type) => {
|
||||||
|
if (type === 'boolean') {
|
||||||
|
configForm.config_value = 'true'
|
||||||
|
} else if (type === 'number') {
|
||||||
|
configForm.config_value = 0
|
||||||
|
} else if (type === 'json') {
|
||||||
|
configForm.config_value = '{}'
|
||||||
|
} else {
|
||||||
|
configForm.config_value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create'
|
||||||
|
resetConfigForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (config) => {
|
||||||
|
dialogMode.value = 'edit'
|
||||||
|
Object.assign(configForm, {
|
||||||
|
id: config.id,
|
||||||
|
config_key: config.config_key,
|
||||||
|
config_name: config.config_name,
|
||||||
|
config_value: config.config_value,
|
||||||
|
config_type: config.config_type,
|
||||||
|
config_description: config.config_description,
|
||||||
|
is_system: config.is_system
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示配置详情
|
||||||
|
const showConfigDetail = (config) => {
|
||||||
|
currentConfig.value = config
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置配置表单
|
||||||
|
const resetConfigForm = () => {
|
||||||
|
Object.assign(configForm, {
|
||||||
|
config_key: '',
|
||||||
|
config_name: '',
|
||||||
|
config_value: '',
|
||||||
|
config_type: 'string',
|
||||||
|
config_description: '',
|
||||||
|
is_system: 0
|
||||||
|
})
|
||||||
|
if (configFormRef.value) {
|
||||||
|
configFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交配置表单
|
||||||
|
const submitConfig = async () => {
|
||||||
|
if (!configFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await configFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据类型转换配置值
|
||||||
|
let submitData = { ...configForm }
|
||||||
|
if (configForm.config_type === 'number') {
|
||||||
|
submitData.config_value = String(Number(configForm.config_value))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogMode.value === 'create') {
|
||||||
|
await axios.post('/admin/configs', submitData)
|
||||||
|
ElMessage.success('配置创建成功')
|
||||||
|
} else {
|
||||||
|
const { id, ...updateData } = submitData
|
||||||
|
await axios.put(`/admin/configs/${id}`, updateData)
|
||||||
|
ElMessage.success('配置更新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = false
|
||||||
|
getConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交配置失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置
|
||||||
|
const deleteConfig = async (config) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除配置 "${config.config_name}" 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await axios.delete(`/admin/configs/${config.id}`)
|
||||||
|
ElMessage.success('配置删除成功')
|
||||||
|
getConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除配置失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getConfigs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-configs-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value {
|
||||||
|
max-width: 200px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-detail {
|
||||||
|
max-width: 100%;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-preview {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text:hover {
|
||||||
|
color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-warehouses-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>用户库存管理</h2>
|
||||||
|
<el-button type="primary" @click="refreshData">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新数据
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="搜索用户">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.search"
|
||||||
|
placeholder="请输入手机号或用户姓名"
|
||||||
|
clearable
|
||||||
|
style="width: 250px"
|
||||||
|
@keyup.enter="getUserWarehouses"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- <el-form-item label="商品类型">-->
|
||||||
|
<!-- <el-select-->
|
||||||
|
<!-- v-model="searchForm.product_type"-->
|
||||||
|
<!-- placeholder="请选择商品类型"-->
|
||||||
|
<!-- clearable-->
|
||||||
|
<!-- style="width: 150px"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- <el-option label="一级商品" :value="1" />-->
|
||||||
|
<!-- <el-option label="二级商品" :value="2" />-->
|
||||||
|
<!-- </el-select>-->
|
||||||
|
<!-- </el-form-item>-->
|
||||||
|
|
||||||
|
<el-form-item label="库存状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="库存中" :value="1" />
|
||||||
|
<el-option label="已出售" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getUserWarehouses" :loading="loading">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 用户库存统计卡片 -->
|
||||||
|
<div class="stats-cards">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<el-statistic title="有库存用户数" :value="totalUsers || 0" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<el-statistic title="库存商品总数" :value="totalQuantity || 0" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<!-- <el-col :span="6">-->
|
||||||
|
<!-- <el-card class="stat-card">-->
|
||||||
|
<!-- <el-statistic title="库存总价值" :value="totalValue || 0" prefix="¥" :precision="2" />-->
|
||||||
|
<!-- </el-card>-->
|
||||||
|
<!-- </el-col>-->
|
||||||
|
<!-- <el-col :span="6">-->
|
||||||
|
<!-- <el-card class="stat-card">-->
|
||||||
|
<!-- <el-statistic title="已出售数量" :value="soldCount || 0" />-->
|
||||||
|
<!-- </el-card>-->
|
||||||
|
<!-- </el-col>-->
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户库存列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="userWarehouses"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column label="用户信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-name">{{ row.customer_name || row.real_name || '未知用户' }}</div>
|
||||||
|
<div class="user-phone">{{ row.phone }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_type" label="商品类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.product_type === 1 ? 'primary' : 'success'" size="small">
|
||||||
|
{{ row.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="total_quantity" label="库存数量" width="100" sortable>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="{ 'low-stock': row.total_quantity < 10 }">{{ row.total_quantity }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="total_value" label="库存价值" width="120" sortable>
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ formatAmount(row.total_value) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.status === 1 ? '库存中' : '已出售' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- <el-table-column label="统计信息" width="200">-->
|
||||||
|
<!-- <template #default="{ row }">-->
|
||||||
|
<!-- <div class="stats-info">-->
|
||||||
|
<!-- <div>一级: {{ row.primary_product_count }}</div>-->
|
||||||
|
<!-- <div>二级: {{ row.secondary_product_count }}</div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-table-column>-->
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="viewUserDetail(row)">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getUserWarehouses"
|
||||||
|
@current-change="getUserWarehouses"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 用户库存详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
:title="`用户库存详情 - ${currentUser?.customer_name || currentUser?.real_name || ''}`"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<div v-if="currentUserWarehouses.length > 0" class="warehouse-detail">
|
||||||
|
<el-descriptions :column="2" border class="mb-4">
|
||||||
|
<el-descriptions-item label="客户姓名">{{ currentUser.customer_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="真实姓名">{{ currentUser.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">{{ currentUser.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="库存总价值">
|
||||||
|
<span class="text-primary">¥{{ formatAmount(currentUserTotalValue) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-table :data="currentUserWarehouses" stripe>
|
||||||
|
<el-table-column prop="product_type" label="商品类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.product_type === 1 ? 'primary' : 'success'" size="small">
|
||||||
|
{{ row.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_name" label="商品名称" min-width="200" />
|
||||||
|
|
||||||
|
<el-table-column prop="total_quantity" label="数量" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="total_value" label="价值" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ formatAmount(row.total_value) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.status === 1 ? '库存中' : '已出售' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-data">
|
||||||
|
<el-empty description="暂无库存数据" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const userWarehouses = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentUser = ref({})
|
||||||
|
const currentUserWarehouses = ref([])
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
search: '',
|
||||||
|
product_type: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
const totalUsers = ref(0)
|
||||||
|
const totalQuantity = ref(0)
|
||||||
|
const totalValue = ref(0)
|
||||||
|
const soldCount = ref(0)
|
||||||
|
|
||||||
|
// 计算当前用户库存总价值
|
||||||
|
const currentUserTotalValue = computed(() => {
|
||||||
|
return currentUserWarehouses.value.reduce((sum, item) => {
|
||||||
|
return sum + (item.total_value || 0)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户库存列表
|
||||||
|
const getUserWarehouses = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/warehouse/users', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const data = response.data.data
|
||||||
|
// 如果后端返回的数据超过pageSize,只显示pageSize条
|
||||||
|
const list = data.list || []
|
||||||
|
if (list.length > pagination.pageSize) {
|
||||||
|
userWarehouses.value = list.slice(0, pagination.pageSize)
|
||||||
|
} else {
|
||||||
|
userWarehouses.value = list
|
||||||
|
}
|
||||||
|
pagination.total = data.total || 0
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
calculateStats()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户库存列表失败:', error)
|
||||||
|
ElMessage.error('获取用户库存列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const calculateStats = () => {
|
||||||
|
const warehouses = userWarehouses.value
|
||||||
|
|
||||||
|
// 去重计算用户数(根据 user_id)
|
||||||
|
const uniqueUsers = new Set(warehouses.map(w => w.user_id))
|
||||||
|
totalUsers.value = uniqueUsers.size
|
||||||
|
|
||||||
|
// 计算总数量(只统计库存中的)
|
||||||
|
const stockItems = warehouses.filter(w => w.status === 1)
|
||||||
|
totalQuantity.value = stockItems.reduce((sum, w) => sum + (w.total_quantity || 0), 0)
|
||||||
|
|
||||||
|
// 计算总价值(只统计库存中的)
|
||||||
|
totalValue.value = stockItems.reduce((sum, w) => sum + (w.total_value || 0), 0)
|
||||||
|
|
||||||
|
// 计算已出售数量
|
||||||
|
soldCount.value = warehouses
|
||||||
|
.filter(w => w.status === 0)
|
||||||
|
.reduce((sum, w) => sum + (w.total_quantity || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
search: '',
|
||||||
|
product_type: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getUserWarehouses()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
getUserWarehouses()
|
||||||
|
ElMessage.success('数据已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看用户详情
|
||||||
|
const viewUserDetail = async (warehouse) => {
|
||||||
|
currentUser.value = {
|
||||||
|
user_id: warehouse.user_id,
|
||||||
|
customer_name: warehouse.customer_name,
|
||||||
|
real_name: warehouse.real_name,
|
||||||
|
phone: warehouse.phone
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该用户的所有库存(不区分商品类型和状态)
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
search: warehouse.phone,
|
||||||
|
page: 1,
|
||||||
|
page_size: 1000
|
||||||
|
}
|
||||||
|
// 清除筛选条件,获取该用户的所有库存
|
||||||
|
const searchKey = searchForm.search
|
||||||
|
const productType = searchForm.product_type
|
||||||
|
const status = searchForm.status
|
||||||
|
|
||||||
|
searchForm.search = warehouse.phone
|
||||||
|
searchForm.product_type = ''
|
||||||
|
searchForm.status = ''
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/warehouse/users', {
|
||||||
|
params: {
|
||||||
|
search: warehouse.phone,
|
||||||
|
page: 1,
|
||||||
|
page_size: 1000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
currentUserWarehouses.value = response.data.data.list || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复原有的搜索条件
|
||||||
|
searchForm.search = searchKey
|
||||||
|
searchForm.product_type = productType
|
||||||
|
searchForm.status = status
|
||||||
|
|
||||||
|
detailVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户库存详情失败:', error)
|
||||||
|
ElMessage.error('获取用户库存详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getUserWarehouses()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-warehouses-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-phone {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-stock {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards .el-col {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,950 @@
|
|||||||
|
<template>
|
||||||
|
<div class="users-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>用户管理</h2>
|
||||||
|
<el-button v-if="authStore.isAdmin" type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加用户
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="真实姓名">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.real_name"
|
||||||
|
placeholder="请输入真实姓名"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户角色">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.system_role"
|
||||||
|
placeholder="请选择角色"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<!-- <el-option label="管理员" value="0" />-->
|
||||||
|
<el-option label="代理商" value="1" />
|
||||||
|
<el-option label="普通用户" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="身份码">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.identity_code"
|
||||||
|
placeholder="请输入身份码"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getUsers">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 用户列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="users"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||||
|
|
||||||
|
<el-table-column prop="phone" label="手机号" width="140" />
|
||||||
|
|
||||||
|
<el-table-column prop="customer_name" label="客户姓名" width="120" />
|
||||||
|
|
||||||
|
<el-table-column prop="real_name" label="真实姓名" width="120" />
|
||||||
|
|
||||||
|
<el-table-column prop="system_role" label="角色" width="100" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="getRoleTagType(row.system_role)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getRoleName(row.system_role) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="current_points" label="当前积分" width="100" sortable="custom" />
|
||||||
|
|
||||||
|
<el-table-column prop="identity_code" label="身份码" width="120" />
|
||||||
|
|
||||||
|
<el-table-column prop="company_name" label="公司名称" min-width="150" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="referrer_identity_code" label="推荐人身份码" width="140" />
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="80" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="row.status === 1 ? 'success' : 'danger'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="expiry_date" label="过期时间" width="160" sortable="custom">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.expiry_date" :class="{ 'expired-text': isExpired(row.expiry_date) }">
|
||||||
|
{{ formatDate(row.expiry_date) }}
|
||||||
|
</span>
|
||||||
|
<el-tag v-else type="success" size="small">永不过期</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showUserDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="showEditDialog(row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="showPaymentDialog(row)"
|
||||||
|
>
|
||||||
|
付款码
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin && row.system_role !== 0"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="showExpiryDialog(row)"
|
||||||
|
>
|
||||||
|
设置过期
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin && row.system_role !== 0"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="danger-text"
|
||||||
|
@click="deleteUser(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getUsers"
|
||||||
|
@current-change="getUsers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑用户对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogMode === 'create' ? '添加用户' : '编辑用户'"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="userFormRef"
|
||||||
|
:model="userForm"
|
||||||
|
:rules="userRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
:disabled="dialogMode === 'edit'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户角色" prop="system_role">
|
||||||
|
<el-select v-model="userForm.system_role" placeholder="请选择角色">
|
||||||
|
<!-- <el-option label="管理员" :value="0" />-->
|
||||||
|
<el-option label="代理商" :value="1" />
|
||||||
|
<el-option label="普通用户" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="客户姓名" prop="customer_name">
|
||||||
|
<el-input v-model="userForm.customer_name" placeholder="请输入客户姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="真实姓名" prop="real_name">
|
||||||
|
<el-input v-model="userForm.real_name" placeholder="请输入真实姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="公司名称">
|
||||||
|
<el-input v-model="userForm.company_name" placeholder="请输入公司名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="推荐人身份码">
|
||||||
|
<el-input v-model="userForm.referrer_identity_code" placeholder="请输入推荐人身份码" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="个人介绍">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.personal_intro"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入个人介绍"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="userForm.status">
|
||||||
|
<el-radio :label="1">启用</el-radio>
|
||||||
|
<el-radio :label="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitUser">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="用户详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="用户ID">{{ currentUser.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">{{ currentUser.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户角色">
|
||||||
|
<el-tag :type="getRoleTagType(currentUser.system_role)">
|
||||||
|
{{ getRoleName(currentUser.system_role) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="currentUser.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ currentUser.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="客户姓名">{{ currentUser.customer_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="真实姓名">{{ currentUser.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前积分">{{ currentUser.current_points }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="身份码">{{ currentUser.identity_code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司名称" :span="2">{{ currentUser.company_name || '未填写' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="推荐人身份码" :span="2">{{ currentUser.referrer_identity_code || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="个人介绍" :span="2">{{ currentUser.personal_intro || '未填写' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="过期时间" :span="2">
|
||||||
|
<span v-if="currentUser.expiry_date" :class="{ 'expired-text': isExpired(currentUser.expiry_date) }">
|
||||||
|
{{ formatDate(currentUser.expiry_date) }}
|
||||||
|
</span>
|
||||||
|
<el-tag v-else type="success" size="small">永不过期</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="营业执照" :span="2">
|
||||||
|
|
||||||
|
<el-image
|
||||||
|
:src="currentUser.business_license_image"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="[currentUser.business_license_image]"
|
||||||
|
style="width: 248px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="身份证正面" :span="2">
|
||||||
|
|
||||||
|
<el-image
|
||||||
|
:src="currentUser.id_card_front_image"
|
||||||
|
:preview-src-list="[currentUser.id_card_front_image]"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 248px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="身份证反面" :span="2">
|
||||||
|
|
||||||
|
<el-image
|
||||||
|
:src="currentUser.id_card_back_image"
|
||||||
|
:preview-src-list="[currentUser.id_card_back_image]"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 248px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间" :span="2">{{ formatDate(currentUser.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间" :span="2">{{ formatDate(currentUser.updated_at) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 设置用户过期时间对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="expiryVisible"
|
||||||
|
title="设置用户过期时间"
|
||||||
|
width="400px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="expiryFormRef"
|
||||||
|
:model="expiryForm"
|
||||||
|
label-width="80px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户">
|
||||||
|
<span>{{ expiryUser.customer_name || expiryUser.real_name || expiryUser.phone }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="过期设置">
|
||||||
|
<el-radio-group v-model="expiryForm.type">
|
||||||
|
<el-radio label="never">永不过期</el-radio>
|
||||||
|
<el-radio label="custom">设置过期时间</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="expiryForm.type === 'custom'" label="过期时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="expiryForm.expiryDate"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择过期时间"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="expiryVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="expirySubmitting" @click="submitExpiry">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户付款码管理对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="paymentVisible"
|
||||||
|
title="用户付款码管理"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div class="payment-info">
|
||||||
|
<p><strong>用户:</strong>{{ paymentUser.customer_name || paymentUser.real_name || paymentUser.phone }}</p>
|
||||||
|
<p><strong>手机号:</strong>{{ paymentUser.phone }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="paymentFormRef"
|
||||||
|
:model="paymentForm"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<el-form-item label="主收款码">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="paymentForm.main_payment_qr_image"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
:preview-src-list="paymentForm.main_payment_qr_image ? [getFullImageUrl(paymentForm.main_payment_qr_image)] : []"
|
||||||
|
/>
|
||||||
|
<div class="upload-tip">支持jpg、png格式,建议尺寸200x200px</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="副收款码">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="paymentForm.sub_payment_qr_image"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
:preview-src-list="paymentForm.sub_payment_qr_image ? [getFullImageUrl(paymentForm.sub_payment_qr_image)] : []"
|
||||||
|
/>
|
||||||
|
<div class="upload-tip">支持jpg、png格式,建议尺寸200x200px</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="paymentVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="paymentSubmitting" @click="updatePaymentInfo">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import ImageUpload from '@/components/ImageUpload.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const users = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const expiryVisible = ref(false)
|
||||||
|
const paymentVisible = ref(false)
|
||||||
|
const dialogMode = ref('create')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const expirySubmitting = ref(false)
|
||||||
|
const paymentSubmitting = ref(false)
|
||||||
|
const userFormRef = ref()
|
||||||
|
const expiryFormRef = ref()
|
||||||
|
const paymentFormRef = ref()
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
phone: '',
|
||||||
|
real_name: '',
|
||||||
|
system_role: '',
|
||||||
|
identity_code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 用户表单
|
||||||
|
const userForm = reactive({
|
||||||
|
phone: '',
|
||||||
|
password: '',
|
||||||
|
system_role: 2,
|
||||||
|
customer_name: '',
|
||||||
|
real_name: '',
|
||||||
|
company_name: '',
|
||||||
|
referrer_identity_code: '',
|
||||||
|
personal_intro: '',
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的用户
|
||||||
|
const currentUser = ref({})
|
||||||
|
|
||||||
|
// 当前设置过期时间的用户
|
||||||
|
const expiryUser = ref({})
|
||||||
|
|
||||||
|
// 过期时间表单
|
||||||
|
const expiryForm = reactive({
|
||||||
|
type: 'never',
|
||||||
|
expiryDate: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前编辑付款码的用户
|
||||||
|
const paymentUser = ref({})
|
||||||
|
|
||||||
|
// 付款码表单
|
||||||
|
const paymentForm = reactive({
|
||||||
|
main_payment_qr_image: '',
|
||||||
|
sub_payment_qr_image: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const userRules = {
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
|
||||||
|
system_role: [
|
||||||
|
{ required: true, message: '请选择用户角色', trigger: 'change' }
|
||||||
|
],
|
||||||
|
customer_name: [
|
||||||
|
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const getUsers = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/users', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
users.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
phone: '',
|
||||||
|
real_name: '',
|
||||||
|
system_role: '',
|
||||||
|
identity_code: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理排序
|
||||||
|
const handleSortChange = ({ prop, order }) => {
|
||||||
|
if (!prop) return
|
||||||
|
|
||||||
|
// 复制用户数组进行排序
|
||||||
|
const sortedUsers = [...users.value]
|
||||||
|
|
||||||
|
sortedUsers.sort((a, b) => {
|
||||||
|
let aVal = a[prop]
|
||||||
|
let bVal = b[prop]
|
||||||
|
|
||||||
|
// 处理不同类型的字段排序
|
||||||
|
switch (prop) {
|
||||||
|
case 'system_role':
|
||||||
|
// 角色排序:管理员(0) < 代理商(1) < 普通用户(2)
|
||||||
|
aVal = aVal || 999
|
||||||
|
bVal = bVal || 999
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
// 状态排序:禁用(0) < 启用(1)
|
||||||
|
aVal = aVal || 0
|
||||||
|
bVal = bVal || 0
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'expiry_date':
|
||||||
|
// 过期时间排序:永不过期(null) < 已过期 < 未过期
|
||||||
|
if (!aVal && !bVal) {
|
||||||
|
aVal = bVal = 0 // 都是永不过期,相等
|
||||||
|
} else if (!aVal) {
|
||||||
|
aVal = -1 // 永不过期排在前面
|
||||||
|
} else if (!bVal) {
|
||||||
|
bVal = -1
|
||||||
|
} else {
|
||||||
|
const aExpired = isExpired(aVal)
|
||||||
|
const bExpired = isExpired(bVal)
|
||||||
|
if (aExpired && !bExpired) {
|
||||||
|
aVal = 1 // 已过期排后面
|
||||||
|
bVal = 0 // 未过期排前面
|
||||||
|
} else if (!aExpired && bExpired) {
|
||||||
|
aVal = 0
|
||||||
|
bVal = 1
|
||||||
|
} else {
|
||||||
|
// 都是已过期或都是未过期,按时间排序
|
||||||
|
aVal = new Date(aVal).getTime()
|
||||||
|
bVal = new Date(bVal).getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'current_points':
|
||||||
|
// 积分排序:null转为0
|
||||||
|
aVal = aVal || 0
|
||||||
|
bVal = bVal || 0
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 默认排序处理
|
||||||
|
if (aVal == null) aVal = ''
|
||||||
|
if (bVal == null) bVal = ''
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较值
|
||||||
|
if (aVal < bVal) {
|
||||||
|
return order === 'ascending' ? -1 : 1
|
||||||
|
} else if (aVal > bVal) {
|
||||||
|
return order === 'ascending' ? 1 : -1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新用户列表
|
||||||
|
users.value = sortedUsers
|
||||||
|
|
||||||
|
// 提示用户
|
||||||
|
if (order) {
|
||||||
|
const orderText = order === 'ascending' ? '升序' : '降序'
|
||||||
|
console.log(`已按${prop}进行${orderText}排序`)
|
||||||
|
} else {
|
||||||
|
console.log('已取消排序')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色名称
|
||||||
|
const getRoleName = (role) => {
|
||||||
|
const roleMap = { 0: '管理员', 1: '代理商', 2: '普通用户' }
|
||||||
|
return roleMap[role] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色标签类型
|
||||||
|
const getRoleTagType = (role) => {
|
||||||
|
const typeMap = { 0: '', 1: 'warning', 2: 'info' }
|
||||||
|
return typeMap[role] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否已过期
|
||||||
|
const isExpired = (expiryDate) => {
|
||||||
|
if (!expiryDate) return false
|
||||||
|
return dayjs(expiryDate).isBefore(dayjs())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用过去的日期
|
||||||
|
const disabledDate = (time) => {
|
||||||
|
return time.getTime() < Date.now() - 8.64e7 // 禁用昨天之前的日期
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create'
|
||||||
|
resetUserForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (user) => {
|
||||||
|
dialogMode.value = 'edit'
|
||||||
|
Object.assign(userForm, {
|
||||||
|
...user,
|
||||||
|
password: '' // 编辑时不显示密码
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示用户详情
|
||||||
|
const showUserDetail = (user) => {
|
||||||
|
currentUser.value = user
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示设置过期时间对话框
|
||||||
|
const showExpiryDialog = (user) => {
|
||||||
|
expiryUser.value = user
|
||||||
|
expiryForm.type = user.expiry_date ? 'custom' : 'never'
|
||||||
|
expiryForm.expiryDate = user.expiry_date ? dayjs(user.expiry_date).format('YYYY-MM-DDTHH:mm:ssZ') : null
|
||||||
|
expiryVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置用户表单
|
||||||
|
const resetUserForm = () => {
|
||||||
|
Object.assign(userForm, {
|
||||||
|
phone: '',
|
||||||
|
password: '',
|
||||||
|
system_role: 2,
|
||||||
|
customer_name: '',
|
||||||
|
real_name: '',
|
||||||
|
company_name: '',
|
||||||
|
referrer_identity_code: '',
|
||||||
|
personal_intro: '',
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
if (userFormRef.value) {
|
||||||
|
userFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交用户表单
|
||||||
|
const submitUser = async () => {
|
||||||
|
if (!userFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await userFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (dialogMode.value === 'create') {
|
||||||
|
Object.assign(userForm, {
|
||||||
|
id: null,
|
||||||
|
status: 0
|
||||||
|
})
|
||||||
|
await axios.post('/admin/users', userForm)
|
||||||
|
ElMessage.success('用户创建成功')
|
||||||
|
} else {
|
||||||
|
const { id, ...updateData } = userForm
|
||||||
|
if (!updateData.password) {
|
||||||
|
delete updateData.password
|
||||||
|
}
|
||||||
|
await axios.put(`/admin/users/${id}`, updateData)
|
||||||
|
ElMessage.success('用户更新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = false
|
||||||
|
getUsers()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交用户失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交过期时间设置
|
||||||
|
const submitExpiry = async () => {
|
||||||
|
expirySubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expiryDate = expiryForm.type === 'never' ? null : expiryForm.expiryDate
|
||||||
|
|
||||||
|
await axios.put(`/admin/users/${expiryUser.value.id}/expiry`, {
|
||||||
|
expiry_date: expiryDate
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('用户过期时间设置成功')
|
||||||
|
expiryVisible.value = false
|
||||||
|
getUsers() // 刷新列表
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置过期时间失败:', error)
|
||||||
|
ElMessage.error('设置过期时间失败')
|
||||||
|
} finally {
|
||||||
|
expirySubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示付款码管理对话框
|
||||||
|
const showPaymentDialog = async (user) => {
|
||||||
|
paymentUser.value = user
|
||||||
|
|
||||||
|
// 获取用户当前的付款信息(直接从用户信息中获取)
|
||||||
|
Object.assign(paymentForm, {
|
||||||
|
main_payment_qr_image: user.main_payment_qr_image || '',
|
||||||
|
sub_payment_qr_image: user.sub_payment_qr_image || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
paymentVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置付款表单
|
||||||
|
const resetPaymentForm = () => {
|
||||||
|
Object.assign(paymentForm, {
|
||||||
|
main_payment_qr_image: '',
|
||||||
|
sub_payment_qr_image: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户付款信息
|
||||||
|
const updatePaymentInfo = async () => {
|
||||||
|
paymentSubmitting.value = true
|
||||||
|
try {
|
||||||
|
paymentUser.value.main_payment_qr_image= paymentForm.main_payment_qr_image
|
||||||
|
paymentUser.value.sub_payment_qr_image= paymentForm.sub_payment_qr_image
|
||||||
|
const { data } = await axios.put(`/admin/users/${paymentUser.value.id}`, paymentUser.value)
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
ElMessage.success('用户付款信息更新成功')
|
||||||
|
paymentVisible.value = false
|
||||||
|
getUsers() // 刷新用户列表
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data.message || '更新失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户付款信息失败:', error)
|
||||||
|
ElMessage.error('更新失败')
|
||||||
|
} finally {
|
||||||
|
paymentSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完整图片URL
|
||||||
|
const getFullImageUrl = (url) => {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const deleteUser = async (user) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除用户 "${user.customer_name || user.phone}" 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await axios.delete(`/admin/users/${user.id}`)
|
||||||
|
ElMessage.success('用户删除成功')
|
||||||
|
getUsers()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除用户失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.users-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text:hover {
|
||||||
|
color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,788 @@
|
|||||||
|
<template>
|
||||||
|
<div class="purchase-orders-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>买单管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="订单编号">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.order_no"
|
||||||
|
placeholder="请输入订单编号"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="订单状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.order_status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="待付款" value="0" />
|
||||||
|
<el-option label="待确认" value="1" />
|
||||||
|
<el-option label="已完成" value="2" />
|
||||||
|
<el-option label="已取消" value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="商品类型">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.product_type"
|
||||||
|
placeholder="请选择类型"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="一级商品" value="1" />
|
||||||
|
<el-option label="二级商品" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="卖家名字">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.seller_name"
|
||||||
|
placeholder="请输入卖家名字"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="买家名字">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.buyer_name"
|
||||||
|
placeholder="请输入买家名字"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getOrders">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="orders"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="order_no" label="订单编号" width="180" />
|
||||||
|
|
||||||
|
<el-table-column label="买家信息" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.buyer?.real_name || row.buyer?.phone }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ row.buyer?.phone }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="卖家信息" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.seller">
|
||||||
|
{{ row.seller?.real_name || row.seller?.phone }}
|
||||||
|
<div class="text-xs text-gray-500">{{ row.seller?.phone }}</div>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400">平台商品</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="product_type" label="商品类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.product_type === 1 ? 'primary' : 'warning'" size="small">
|
||||||
|
{{ row.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="unit_price" label="单价" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ formatAmount(row.unit_price) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="quantity" label="数量" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="total_amount" label="总金额" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-semibold text-red-500">¥{{ formatAmount(row.total_amount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="order_status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.order_status)" size="small">
|
||||||
|
{{ getStatusName(row.order_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showOrderDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" size="small" @click="showMessages(row)">
|
||||||
|
留言
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin && row.order_status === 1"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="showStatusDialog(row)"
|
||||||
|
>
|
||||||
|
处理
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getOrders"
|
||||||
|
@current-change="getOrders"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="买单详情"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单编号" :span="2">{{ currentOrder.order_no }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="买家姓名">{{ currentOrder.buyer?.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="买家手机">{{ currentOrder.buyer?.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家姓名">{{ currentOrder.seller?.real_name || '平台' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家手机">{{ currentOrder.seller?.phone || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称" :span="2">{{ currentOrder.product_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品类型">
|
||||||
|
<el-tag :type="currentOrder.product_type === 1 ? 'primary' : 'warning'">
|
||||||
|
{{ currentOrder.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单状态">
|
||||||
|
<el-tag :type="getStatusTagType(currentOrder.order_status)">
|
||||||
|
{{ getStatusName(currentOrder.order_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="单价">¥{{ formatAmount(currentOrder.unit_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数量">{{ currentOrder.quantity }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总金额" :span="2">
|
||||||
|
<span class="text-red-500 font-semibold text-lg">¥{{ formatAmount(currentOrder.total_amount) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货地址" :span="2">
|
||||||
|
<div v-if="currentOrder.address">
|
||||||
|
{{ currentOrder.address.consignee_name }} {{ currentOrder.address.consignee_phone }}<br>
|
||||||
|
{{ currentOrder.address.province }}{{ currentOrder.address.city }}{{ currentOrder.address.district }}{{ currentOrder.address.detail_address }}
|
||||||
|
</div>
|
||||||
|
<span v-else>暂无</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ currentOrder.remark || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="确认时间">{{ formatDate(currentOrder.confirm_time) || '未确认' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<div v-if="currentOrder.product_images" class="mt-4">
|
||||||
|
<h4>商品图片</h4>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="(image, index) in getProductImages(currentOrder.product_images)"
|
||||||
|
:key="index"
|
||||||
|
:src="image"
|
||||||
|
:preview-src-list="getProductImages(currentOrder.product_images)"
|
||||||
|
fit="cover"
|
||||||
|
class="product-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4>付款凭证</h4>
|
||||||
|
<div v-if="currentOrder.payment_proof_image" class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
|
||||||
|
:src="currentOrder.payment_proof_image"
|
||||||
|
alt="无"
|
||||||
|
fit="cover"
|
||||||
|
:preview-src-list="[currentOrder.payment_proof_image]"
|
||||||
|
class="product-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
无
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订单状态处理对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="statusVisible"
|
||||||
|
title="处理订单"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="statusFormRef"
|
||||||
|
:model="statusForm"
|
||||||
|
:rules="statusRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="处理结果" prop="order_status">
|
||||||
|
<el-radio-group v-model="statusForm.order_status">
|
||||||
|
<el-radio :label="2">确认完成</el-radio>
|
||||||
|
<el-radio :label="3">取消订单</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="处理备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="statusForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入处理备注"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="statusVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitStatus">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订单留言对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="messagesVisible"
|
||||||
|
:title="`订单留言 - ${currentOrder.order_no}`"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<!-- 留言列表 -->
|
||||||
|
<div class="messages-container">
|
||||||
|
<div class="messages-list">
|
||||||
|
<div
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
class="message-item"
|
||||||
|
:class="{ 'admin-message': message.user?.system_role === 0 || message.user?.system_role === 1 }"
|
||||||
|
>
|
||||||
|
<div class="message-header">
|
||||||
|
<span class="user-name">
|
||||||
|
{{ message.user?.real_name || message.user?.customer_name }}
|
||||||
|
<el-tag
|
||||||
|
v-if="message.user?.system_role === 0"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
管理员
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="message.user?.system_role === 1"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
代理商
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<span class="message-time">{{ formatDate(message.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">{{ message.content }}</div>
|
||||||
|
<!-- 留言图片 -->
|
||||||
|
<div v-if="message.images" class="message-images">
|
||||||
|
<el-image
|
||||||
|
v-for="(image, index) in getMessageImages(message.images)"
|
||||||
|
:key="index"
|
||||||
|
:src="(image)"
|
||||||
|
:preview-src-list="getMessageImages(message.images)"
|
||||||
|
:initial-index="index"
|
||||||
|
class="message-image"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="messages.length === 0" class="empty-messages">
|
||||||
|
暂无留言
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 发送留言 -->
|
||||||
|
<div class="send-message">
|
||||||
|
<el-form :model="messageForm" @submit.prevent="sendMessage">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="messageForm.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入留言内容(最多500字)"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="sendingMessage"
|
||||||
|
@click="sendMessage"
|
||||||
|
:disabled="!messageForm.content.trim()"
|
||||||
|
>
|
||||||
|
发送留言
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="messagesVisible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const orders = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const statusVisible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const statusFormRef = ref()
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
order_no: '',
|
||||||
|
order_status: '',
|
||||||
|
product_type: '',
|
||||||
|
seller_name: '',
|
||||||
|
buyer_name: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的订单
|
||||||
|
const currentOrder = ref({})
|
||||||
|
|
||||||
|
// 状态处理表单
|
||||||
|
const statusForm = reactive({
|
||||||
|
order_status: 2,
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态处理验证规则
|
||||||
|
const statusRules = {
|
||||||
|
order_status: [
|
||||||
|
{ required: true, message: '请选择处理结果', trigger: 'change' }
|
||||||
|
],
|
||||||
|
remark: [
|
||||||
|
{ required: true, message: '请输入处理备注', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 留言相关数据
|
||||||
|
const messagesVisible = ref(false)
|
||||||
|
const messages = ref([])
|
||||||
|
const sendingMessage = ref(false)
|
||||||
|
const messageForm = reactive({
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const getOrders = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/orders/purchase', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
orders.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取买单列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
order_no: '',
|
||||||
|
order_status: '',
|
||||||
|
product_type: '',
|
||||||
|
seller_name: '',
|
||||||
|
buyer_name: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态名称
|
||||||
|
const getStatusName = (status) => {
|
||||||
|
const statusMap = { 0: '待付款', 1: '待确认', 2: '已完成', 3: '已取消' }
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusTagType = (status) => {
|
||||||
|
const typeMap = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'danger' }
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取留言图片列表
|
||||||
|
const getMessageImages = (images) => {
|
||||||
|
if (!images) return []
|
||||||
|
return images.split(',').filter(img => img.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (imageUrl) => {
|
||||||
|
if (!imageUrl) return ''
|
||||||
|
if (imageUrl.startsWith('http')) return imageUrl
|
||||||
|
return `${axios.defaults.baseURL.replace('/back', '')}/file/${imageUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品图片列表
|
||||||
|
const getProductImages = (images) => {
|
||||||
|
if (!images) return []
|
||||||
|
return images.split(',').filter(img => img.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示订单详情
|
||||||
|
const showOrderDetail = (order) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示状态处理对话框
|
||||||
|
const showStatusDialog = (order) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
statusForm.order_status = 2
|
||||||
|
statusForm.remark = ''
|
||||||
|
statusVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交状态处理
|
||||||
|
const submitStatus = async () => {
|
||||||
|
if (!statusFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await statusFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(`/admin/orders/purchase/${currentOrder.value.id}/status`, statusForm)
|
||||||
|
ElMessage.success('订单处理成功')
|
||||||
|
statusVisible.value = false
|
||||||
|
getOrders()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理订单失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示留言对话框
|
||||||
|
const showMessages = async (order) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
messagesVisible.value = true
|
||||||
|
await getMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单留言
|
||||||
|
const getMessages = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||||
|
params: {
|
||||||
|
type: 'purchase'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
messages.value = response.data.data.list || []
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.message || '获取留言失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取留言失败:', error)
|
||||||
|
ElMessage.error('获取留言失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送留言
|
||||||
|
const sendMessage = async () => {
|
||||||
|
if (!messageForm.content.trim()) {
|
||||||
|
ElMessage.warning('请输入留言内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendingMessage.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||||
|
content: messageForm.content.trim()
|
||||||
|
}, {
|
||||||
|
params: {
|
||||||
|
type: 'purchase'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
ElMessage.success('留言发送成功')
|
||||||
|
messageForm.content = ''
|
||||||
|
await getMessages() // 刷新留言列表
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.message || '发送留言失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送留言失败:', error)
|
||||||
|
ElMessage.error('发送留言失败')
|
||||||
|
} finally {
|
||||||
|
sendingMessage.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.purchase-orders-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-400 {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 留言相关样式 */
|
||||||
|
.messages-container {
|
||||||
|
max-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-list {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.admin-message {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-messages {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 留言图片样式 */
|
||||||
|
.message-images {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,764 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sales-orders-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>卖单管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="订单编号">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.order_no"
|
||||||
|
placeholder="请输入订单编号"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="订单状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.order_status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="待付款" value="0" />
|
||||||
|
<el-option label="待发货" value="1" />
|
||||||
|
<el-option label="确认完成" value="2" />
|
||||||
|
<el-option label="已取消" value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="卖家名字">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.seller_name"
|
||||||
|
placeholder="请输入卖家名字"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="买家名字">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.buyer_name"
|
||||||
|
placeholder="请输入买家名字"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getOrders">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="orders"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="order_no" label="订单编号" width="180" />
|
||||||
|
|
||||||
|
<el-table-column label="卖家信息" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.seller?.real_name || row.seller?.phone }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ row.seller?.phone }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="买家信息" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.buyer?.real_name || row.buyer?.phone }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ row.buyer?.phone }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="selling_price" label="售价" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ formatAmount(row.selling_price) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="quantity" label="数量" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="total_amount" label="总金额" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-semibold text-red-500">¥{{ formatAmount(row.total_amount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="order_status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.order_status)" size="small">
|
||||||
|
{{ getStatusName(row.order_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showOrderDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" size="small" @click="showMessages(row)">
|
||||||
|
留言
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="authStore.isAdmin && row.order_status === 1"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="showStatusDialog(row)"
|
||||||
|
>
|
||||||
|
处理
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getOrders"
|
||||||
|
@current-change="getOrders"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="卖单详情"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单编号" :span="2">{{ currentOrder.order_no }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家姓名">{{ currentOrder.seller?.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家手机">{{ currentOrder.seller?.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="买家姓名">{{ currentOrder.buyer?.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="买家手机">{{ currentOrder.buyer?.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称" :span="2">{{ currentOrder.product_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单状态" :span="2">
|
||||||
|
<el-tag :type="getStatusTagType(currentOrder.order_status)">
|
||||||
|
{{ getStatusName(currentOrder.order_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="售价">¥{{ formatAmount(currentOrder.selling_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数量">{{ currentOrder.quantity }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总金额" :span="2">
|
||||||
|
<span class="text-red-500 font-semibold text-lg">¥{{ formatAmount(currentOrder.total_amount) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货地址" :span="2">
|
||||||
|
<div v-if="currentOrder.address">
|
||||||
|
{{ currentOrder.address.consignee_name }} {{ currentOrder.address.consignee_phone }}<br>
|
||||||
|
{{ currentOrder.address.province }}{{ currentOrder.address.city }}{{ currentOrder.address.district }}{{ currentOrder.address.detail_address }}
|
||||||
|
</div>
|
||||||
|
<span v-else>暂无</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ currentOrder.remark || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="完成时间">{{ formatDate(currentOrder.complete_time) || '未完成' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 商品信息 -->
|
||||||
|
<div v-if="currentOrder.secondary_product" class="mt-4">
|
||||||
|
<h4>关联商品信息</h4>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="商品ID">{{ currentOrder.secondary_product.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="成本价">¥{{ formatAmount(currentOrder.secondary_product.cost_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品描述" :span="2">{{ currentOrder.secondary_product.product_description || '无' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<div v-if="currentOrder.product_images" class="mt-4">
|
||||||
|
<h4>商品图片</h4>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="(image, index) in getProductImages(currentOrder.product_images)"
|
||||||
|
:key="index"
|
||||||
|
:src="image"
|
||||||
|
:preview-src-list="getProductImages(currentOrder.product_images)"
|
||||||
|
fit="cover"
|
||||||
|
class="product-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4>付款凭证</h4>
|
||||||
|
<div v-if="currentOrder.payment_proof_image" class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
:preview-src-list="[currentOrder.payment_proof_image]"
|
||||||
|
:src="currentOrder.payment_proof_image"
|
||||||
|
alt="无"
|
||||||
|
fit="cover"
|
||||||
|
class="product-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
无
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订单状态处理对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="statusVisible"
|
||||||
|
title="处理订单"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="statusFormRef"
|
||||||
|
:model="statusForm"
|
||||||
|
:rules="statusRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="处理结果" prop="order_status">
|
||||||
|
<el-radio-group v-model="statusForm.order_status">
|
||||||
|
<el-radio :label="2">确认完成</el-radio>
|
||||||
|
<el-radio :label="3">取消订单</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="处理备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="statusForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入处理备注"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="statusVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitStatus">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 订单留言对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="messagesVisible"
|
||||||
|
:title="`订单留言 - ${currentOrder.order_no}`"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<!-- 留言列表 -->
|
||||||
|
<div class="messages-container">
|
||||||
|
<div class="messages-list">
|
||||||
|
<div
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
class="message-item"
|
||||||
|
:class="{ 'admin-message': message.user?.system_role === 0 || message.user?.system_role === 1 }"
|
||||||
|
>
|
||||||
|
<div class="message-header">
|
||||||
|
<span class="user-name">
|
||||||
|
{{ message.user?.real_name || message.user?.customer_name }}
|
||||||
|
<el-tag
|
||||||
|
v-if="message.user?.system_role === 0"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
管理员
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="message.user?.system_role === 1"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
代理商
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<span class="message-time">{{ formatDate(message.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">{{ message.content }}</div>
|
||||||
|
<!-- 留言图片 -->
|
||||||
|
<div v-if="message.images" class="message-images">
|
||||||
|
<el-image
|
||||||
|
v-for="(image, index) in getMessageImages(message.images)"
|
||||||
|
:key="index"
|
||||||
|
:src="(image)"
|
||||||
|
:preview-src-list="getMessageImages(message.images)"
|
||||||
|
:initial-index="index"
|
||||||
|
class="message-image"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="messages.length === 0" class="empty-messages">
|
||||||
|
暂无留言
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 发送留言 -->
|
||||||
|
<div class="send-message">
|
||||||
|
<el-form :model="messageForm" @submit.prevent="sendMessage">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="messageForm.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入留言内容(最多500字)"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="sendingMessage"
|
||||||
|
@click="sendMessage"
|
||||||
|
:disabled="!messageForm.content.trim()"
|
||||||
|
>
|
||||||
|
发送留言
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="messagesVisible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const orders = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const statusVisible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const statusFormRef = ref()
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
order_no: '',
|
||||||
|
order_status: '',
|
||||||
|
seller_name: '',
|
||||||
|
buyer_name: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的订单
|
||||||
|
const currentOrder = ref({})
|
||||||
|
|
||||||
|
// 状态处理表单
|
||||||
|
const statusForm = reactive({
|
||||||
|
order_status: 2,
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态处理验证规则
|
||||||
|
const statusRules = {
|
||||||
|
order_status: [
|
||||||
|
{ required: true, message: '请选择处理结果', trigger: 'change' }
|
||||||
|
],
|
||||||
|
remark: [
|
||||||
|
{ required: true, message: '请输入处理备注', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 留言相关数据
|
||||||
|
const messagesVisible = ref(false)
|
||||||
|
const messages = ref([])
|
||||||
|
const sendingMessage = ref(false)
|
||||||
|
const messageForm = reactive({
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const getOrders = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/orders/sales', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
orders.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取卖单列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
order_no: '',
|
||||||
|
order_status: '',
|
||||||
|
seller_name: '',
|
||||||
|
buyer_name: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态名称
|
||||||
|
const getStatusName = (status) => {
|
||||||
|
const statusMap = { 0: '待付款', 1: '待发货', 2: '确认完成', 3: '已取消' }
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusTagType = (status) => {
|
||||||
|
const typeMap = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'danger' }
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取留言图片列表
|
||||||
|
const getMessageImages = (images) => {
|
||||||
|
if (!images) return []
|
||||||
|
return images.split(',').filter(img => img.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (imageUrl) => {
|
||||||
|
if (!imageUrl) return ''
|
||||||
|
if (imageUrl.startsWith('http')) return imageUrl
|
||||||
|
return `${axios.defaults.baseURL.replace('/back', '')}/file/${imageUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品图片列表
|
||||||
|
const getProductImages = (images) => {
|
||||||
|
if (!images) return []
|
||||||
|
return images.split(',').filter(img => img.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示订单详情
|
||||||
|
const showOrderDetail = (order) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示状态处理对话框
|
||||||
|
const showStatusDialog = (order) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
statusForm.order_status = 2
|
||||||
|
statusForm.remark = ''
|
||||||
|
statusVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交状态处理
|
||||||
|
const submitStatus = async () => {
|
||||||
|
if (!statusFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await statusFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(`/admin/orders/sales/${currentOrder.value.id}/status`, statusForm)
|
||||||
|
ElMessage.success('订单处理成功')
|
||||||
|
statusVisible.value = false
|
||||||
|
getOrders()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理订单失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示留言对话框
|
||||||
|
const showMessages = async (order) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
messagesVisible.value = true
|
||||||
|
await getMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单留言
|
||||||
|
const getMessages = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||||
|
params: {
|
||||||
|
type: 'sales'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
messages.value = response.data.data.list || []
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.message || '获取留言失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取留言失败:', error)
|
||||||
|
ElMessage.error('获取留言失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送留言
|
||||||
|
const sendMessage = async () => {
|
||||||
|
if (!messageForm.content.trim()) {
|
||||||
|
ElMessage.warning('请输入留言内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendingMessage.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||||
|
content: messageForm.content.trim()
|
||||||
|
}, {
|
||||||
|
params: {
|
||||||
|
type: 'sales'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
ElMessage.success('留言发送成功')
|
||||||
|
messageForm.content = ''
|
||||||
|
await getMessages() // 刷新留言列表
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.message || '发送留言失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送留言失败:', error)
|
||||||
|
ElMessage.error('发送留言失败')
|
||||||
|
} finally {
|
||||||
|
sendingMessage.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sales-orders-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 留言相关样式 */
|
||||||
|
.messages-container {
|
||||||
|
max-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-list {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.admin-message {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-messages {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 留言图片样式 */
|
||||||
|
.message-images {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
<template>
|
||||||
|
<div class="categories-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>商品分类管理</h2>
|
||||||
|
<el-button type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加分类
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分类列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="categories"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column label="分类图标" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
v-if="row.category_icon"
|
||||||
|
:src="getImageUrl(row.category_icon)"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 50px; height: 50px; border-radius: 4px;"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-gray-400">无图标</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="category_name" label="分类名称" min-width="200" />
|
||||||
|
|
||||||
|
<el-table-column prop="sort_order" label="排序" width="100" />
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch
|
||||||
|
v-model="row.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
@change="updateStatus(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="danger-text"
|
||||||
|
@click="deleteCategory(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑分类对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogMode === 'create' ? '添加分类' : '编辑分类'"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="categoryFormRef"
|
||||||
|
:model="categoryForm"
|
||||||
|
:rules="categoryRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="分类名称" prop="category_name">
|
||||||
|
<el-input
|
||||||
|
v-model="categoryForm.category_name"
|
||||||
|
placeholder="请输入分类名称"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分类图标">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="categoryForm.category_icon"
|
||||||
|
:limit="1"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="排序序号" prop="sort_order">
|
||||||
|
<el-input-number
|
||||||
|
v-model="categoryForm.sort_order"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="categoryForm.status">
|
||||||
|
<el-radio :label="1">启用</el-radio>
|
||||||
|
<el-radio :label="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitCategory">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import ImageUpload from '@/components/ImageUpload.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const categories = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogMode = ref('create')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const categoryFormRef = ref()
|
||||||
|
|
||||||
|
// 分类表单
|
||||||
|
const categoryForm = reactive({
|
||||||
|
category_name: '',
|
||||||
|
category_icon: '',
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const categoryRules = {
|
||||||
|
category_name: [
|
||||||
|
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||||
|
{ max: 50, message: '分类名称不能超过50个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
sort_order: [
|
||||||
|
{ required: true, message: '请输入排序序号', trigger: 'blur' },
|
||||||
|
{ type: 'number', message: '排序序号必须为数字' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类列表
|
||||||
|
const getCategories = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/admin/products/categories')
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
categories.value = response.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分类列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${window.location.origin}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create'
|
||||||
|
resetCategoryForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (category) => {
|
||||||
|
dialogMode.value = 'edit'
|
||||||
|
Object.assign(categoryForm, {
|
||||||
|
id: category.id,
|
||||||
|
category_name: category.category_name,
|
||||||
|
category_icon: category.category_icon || '',
|
||||||
|
sort_order: category.sort_order,
|
||||||
|
status: category.status
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置分类表单
|
||||||
|
const resetCategoryForm = () => {
|
||||||
|
Object.assign(categoryForm, {
|
||||||
|
category_name: '',
|
||||||
|
category_icon: '',
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1
|
||||||
|
})
|
||||||
|
if (categoryFormRef.value) {
|
||||||
|
categoryFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
// 确保图片上传组件也被重置
|
||||||
|
setTimeout(() => {
|
||||||
|
// 使用 nextTick 确保 DOM 更新完成
|
||||||
|
categoryForm.category_icon = ''
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交分类表单
|
||||||
|
const submitCategory = async () => {
|
||||||
|
if (!categoryFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await categoryFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (dialogMode.value === 'create') {
|
||||||
|
await axios.post('/admin/products/categories', categoryForm)
|
||||||
|
ElMessage.success('分类创建成功')
|
||||||
|
} else {
|
||||||
|
const { id, ...updateData } = categoryForm
|
||||||
|
await axios.put(`/admin/products/categories/${id}`, updateData)
|
||||||
|
ElMessage.success('分类更新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 重置表单,确保下次添加时状态正确
|
||||||
|
resetCategoryForm()
|
||||||
|
getCategories()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交分类失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const updateStatus = async (category) => {
|
||||||
|
try {
|
||||||
|
await axios.put(`/admin/products/categories/${category.id}`, {
|
||||||
|
status: category.status
|
||||||
|
})
|
||||||
|
ElMessage.success('状态更新成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新状态失败:', error)
|
||||||
|
// 恢复原状态
|
||||||
|
category.status = category.status === 1 ? 0 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除分类
|
||||||
|
const deleteCategory = async (category) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除分类 "${category.category_name}" 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await axios.delete(`/admin/products/categories/${category.id}`)
|
||||||
|
ElMessage.success('分类删除成功')
|
||||||
|
getCategories()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除分类失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.categories-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-400 {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text:hover {
|
||||||
|
color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,747 @@
|
|||||||
|
<template>
|
||||||
|
<div class="primary-products-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>一级商品管理</h2>
|
||||||
|
<el-button type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="商品分类">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.category_id"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
:label="category.category_name"
|
||||||
|
:value="category.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.product_name"
|
||||||
|
placeholder="请输入商品名称"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
|
<el-option label="上架" value="1" />
|
||||||
|
<el-option label="下架" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getProducts">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 商品列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="products"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column label="商品图片" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
v-if="getProductImages(row.product_images)[0]"
|
||||||
|
:src="getImageUrl(getProductImages(row.product_images)[0])"
|
||||||
|
:preview-src-list="getProductImages(row.product_images).map(img => getImageUrl(img))"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-gray-400">无图片</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column label="分类" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.category?.category_name || '未分类' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="价格" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>
|
||||||
|
<div class="text-red-500 font-semibold">¥{{ formatAmount(row.current_price) }}</div>
|
||||||
|
<div v-if="row.original_price !== row.current_price" class="text-gray-400 text-xs line-through">
|
||||||
|
¥{{ formatAmount(row.original_price) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="stock_quantity" label="库存" width="100" />
|
||||||
|
|
||||||
|
<el-table-column prop="sales_count" label="销量" width="100" />
|
||||||
|
|
||||||
|
<el-table-column prop="view_count" label="浏览量" width="100" />
|
||||||
|
|
||||||
|
<!-- <el-table-column prop="status" label="状态" width="100">-->
|
||||||
|
<!-- <template #default="{ row }">-->
|
||||||
|
<!-- <el-switch-->
|
||||||
|
<!-- v-model="row.status"-->
|
||||||
|
<!-- :active-value="1"-->
|
||||||
|
<!-- :inactive-value="0"-->
|
||||||
|
<!-- @change="updateStatus(row)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-table-column>-->
|
||||||
|
<!-- -->
|
||||||
|
<!-- <el-table-column prop="is_hot" label="热门" width="100">-->
|
||||||
|
<!-- <template #default="{ row }">-->
|
||||||
|
<!-- <el-switch-->
|
||||||
|
<!-- v-model="row.is_hot"-->
|
||||||
|
<!-- :active-value="1"-->
|
||||||
|
<!-- :inactive-value="0"-->
|
||||||
|
<!-- @change="updateHotStatus(row)"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-table-column>-->
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showProductDetail(row)">
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="danger-text"
|
||||||
|
@click="deleteProduct(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getProducts"
|
||||||
|
@current-change="getProducts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑商品对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogMode === 'create' ? '添加商品' : '编辑商品'"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="productFormRef"
|
||||||
|
:model="productForm"
|
||||||
|
:rules="productRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品名称" prop="product_name">
|
||||||
|
<el-input
|
||||||
|
v-model="productForm.product_name"
|
||||||
|
placeholder="请输入商品名称"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商品分类" prop="category_id">
|
||||||
|
<el-select
|
||||||
|
v-model="productForm.category_id"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
:label="category.category_name"
|
||||||
|
:value="category.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="商品描述">
|
||||||
|
<el-input
|
||||||
|
v-model="productForm.product_description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入商品描述"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="商品图片">
|
||||||
|
<ImageUpload
|
||||||
|
v-model="productForm.product_images"
|
||||||
|
:limit="9"
|
||||||
|
:multiple="true"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="原价" prop="original_price">
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.original_price"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="现价" prop="current_price">
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.current_price"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="库存数量" prop="stock_quantity">
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.stock_quantity"
|
||||||
|
:min="0"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="排序序号">
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.sort_order"
|
||||||
|
:min="0"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="productForm.status">
|
||||||
|
<el-radio :label="1">上架</el-radio>
|
||||||
|
<el-radio :label="0">下架</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="热门商品">
|
||||||
|
<el-switch
|
||||||
|
v-model="productForm.is_hot"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="是"
|
||||||
|
inactive-text="否"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitProduct">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 商品详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="商品详情"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="商品ID">{{ currentProduct.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称">{{ currentProduct.product_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品分类">{{ currentProduct.category?.category_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="currentProduct.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ currentProduct.status === 1 ? '上架' : '下架' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="热门商品">
|
||||||
|
<el-tag :type="currentProduct.is_hot === 1 ? 'warning' : 'info'">
|
||||||
|
{{ currentProduct.is_hot === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原价">¥{{ formatAmount(currentProduct.original_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="现价">¥{{ formatAmount(currentProduct.current_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="库存数量">{{ currentProduct.stock_quantity }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="销量">{{ currentProduct.sales_count }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="浏览量">{{ currentProduct.view_count }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="排序序号">{{ currentProduct.sort_order }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品描述" :span="2">{{ currentProduct.product_description || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentProduct.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDate(currentProduct.updated_at) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<div v-if="getProductImages(currentProduct.product_images).length > 0" class="mt-4">
|
||||||
|
<h4>商品图片</h4>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="(image, index) in getProductImages(currentProduct.product_images)"
|
||||||
|
:key="index"
|
||||||
|
:src="getImageUrl(image)"
|
||||||
|
:preview-src-list="getProductImages(currentProduct.product_images).map(img => getImageUrl(img))"
|
||||||
|
fit="cover"
|
||||||
|
class="product-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import ImageUpload from '@/components/ImageUpload.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const products = ref([])
|
||||||
|
const categories = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const dialogMode = ref('create')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const productFormRef = ref()
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
category_id: '',
|
||||||
|
product_name: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 商品表单
|
||||||
|
const productForm = reactive({
|
||||||
|
product_name: '',
|
||||||
|
category_id: '',
|
||||||
|
product_description: '',
|
||||||
|
product_images: '',
|
||||||
|
original_price: 0,
|
||||||
|
current_price: 0,
|
||||||
|
stock_quantity: 0,
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1,
|
||||||
|
is_hot: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的商品
|
||||||
|
const currentProduct = ref({})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const productRules = {
|
||||||
|
product_name: [
|
||||||
|
{ required: true, message: '请输入商品名称', trigger: 'blur' },
|
||||||
|
{ max: 100, message: '商品名称不能超过100个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
category_id: [
|
||||||
|
{ required: true, message: '请选择商品分类', trigger: 'change' }
|
||||||
|
],
|
||||||
|
original_price: [
|
||||||
|
{ required: true, message: '请输入原价', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0, message: '原价不能小于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
current_price: [
|
||||||
|
{ required: true, message: '请输入现价', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0, message: '现价不能小于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
stock_quantity: [
|
||||||
|
{ required: true, message: '请输入库存数量', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0, message: '库存数量不能小于0', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品分类
|
||||||
|
const getCategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/admin/products/categories')
|
||||||
|
if (response.data.success) {
|
||||||
|
categories.value = response.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分类列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品列表
|
||||||
|
const getProducts = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/products/primary', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
products.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取商品列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
category_id: '',
|
||||||
|
product_name: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${window.location.origin}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品图片列表
|
||||||
|
const getProductImages = (images) => {
|
||||||
|
if (!images) return []
|
||||||
|
return images.split(',').filter(img => img.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create'
|
||||||
|
resetProductForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (product) => {
|
||||||
|
dialogMode.value = 'edit'
|
||||||
|
Object.assign(productForm, {
|
||||||
|
id: product.id,
|
||||||
|
product_name: product.product_name,
|
||||||
|
category_id: product.category_id,
|
||||||
|
product_description: product.product_description,
|
||||||
|
product_images: product.product_images || '',
|
||||||
|
original_price: product.original_price,
|
||||||
|
current_price: product.current_price,
|
||||||
|
stock_quantity: product.stock_quantity,
|
||||||
|
sort_order: product.sort_order,
|
||||||
|
status: product.status,
|
||||||
|
is_hot: product.is_hot || 0
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示商品详情
|
||||||
|
const showProductDetail = (product) => {
|
||||||
|
currentProduct.value = product
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置商品表单
|
||||||
|
const resetProductForm = () => {
|
||||||
|
Object.assign(productForm, {
|
||||||
|
product_name: '',
|
||||||
|
category_id: '',
|
||||||
|
product_description: '',
|
||||||
|
product_images: '',
|
||||||
|
original_price: 0,
|
||||||
|
current_price: 0,
|
||||||
|
stock_quantity: 0,
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1,
|
||||||
|
is_hot: 0
|
||||||
|
})
|
||||||
|
if (productFormRef.value) {
|
||||||
|
productFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交商品表单
|
||||||
|
const submitProduct = async () => {
|
||||||
|
if (!productFormRef.value) return
|
||||||
|
|
||||||
|
const valid = await productFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (dialogMode.value === 'create') {
|
||||||
|
await axios.post('/admin/products/primary', productForm)
|
||||||
|
ElMessage.success('商品创建成功')
|
||||||
|
} else {
|
||||||
|
const { id, ...updateData } = productForm
|
||||||
|
await axios.put(`/admin/products/primary/${id}`, updateData)
|
||||||
|
ElMessage.success('商品更新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = false
|
||||||
|
getProducts()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交商品失败:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const updateStatus = async (product) => {
|
||||||
|
try {
|
||||||
|
await axios.put(`/admin/products/primary/${product.id}`, {
|
||||||
|
status: product.status
|
||||||
|
})
|
||||||
|
ElMessage.success('状态更新成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新状态失败:', error)
|
||||||
|
// 恢复原状态
|
||||||
|
product.status = product.status === 1 ? 0 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新热门状态
|
||||||
|
const updateHotStatus = async (product) => {
|
||||||
|
try {
|
||||||
|
await axios.put(`/admin/products/primary/${product.id}`, {
|
||||||
|
is_hot: product.is_hot
|
||||||
|
})
|
||||||
|
ElMessage.success('热门状态更新成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新热门状态失败:', error)
|
||||||
|
// 恢复原状态
|
||||||
|
product.is_hot = product.is_hot === 1 ? 0 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除商品
|
||||||
|
const deleteProduct = async (product) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除商品 "${product.product_name}" 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await axios.delete(`/admin/products/primary/${product.id}`)
|
||||||
|
ElMessage.success('商品删除成功')
|
||||||
|
getProducts()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除商品失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getCategories()
|
||||||
|
getProducts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.primary-products-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-400 {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-through {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text:hover {
|
||||||
|
color: #f78989;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
<template>
|
||||||
|
<div class="secondary-products-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>二级商品管理</h2>
|
||||||
|
<span class="page-desc">用户上架的商品,仅支持查看</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="卖家ID">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.seller_id"
|
||||||
|
placeholder="请输入卖家ID"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.product_name"
|
||||||
|
placeholder="请输入商品名称"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
|
<el-option label="下架" value="0" />
|
||||||
|
<el-option label="上架" value="1" />
|
||||||
|
<el-option label="已售出" value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getProducts">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 商品列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="products"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
|
||||||
|
<el-table-column label="商品图片" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
v-if="getProductImages(row.product_images)[0]"
|
||||||
|
:src="getImageUrl(getProductImages(row.product_images)[0])"
|
||||||
|
:preview-src-list="getProductImages(row.product_images).map(img => getImageUrl(img))"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-gray-400">无图片</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column label="卖家信息" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.seller?.customer_name || row.seller?.phone }}</div>
|
||||||
|
<div class="text-xs text-gray-500">ID: {{ row.seller_id }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="原商品类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.original_product_type === 1 ? 'primary' : 'warning'" size="small">
|
||||||
|
{{ row.original_product_type === 1 ? '一级商品' : '二级商品' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="价格信息" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>
|
||||||
|
<div class="text-green-600 font-semibold">售价: ¥{{ formatAmount(row.selling_price) }}</div>
|
||||||
|
<div class="text-gray-500 text-xs">成本: ¥{{ formatAmount(row.cost_price) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="quantity" label="数量" width="80" />
|
||||||
|
|
||||||
|
<el-table-column prop="view_count" label="浏览量" width="100" />
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.status)" size="small">
|
||||||
|
{{ getStatusName(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="text" size="small" @click="showProductDetail(row)">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="getProducts"
|
||||||
|
@current-change="getProducts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 商品详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="二级商品详情"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="商品ID">{{ currentProduct.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称">{{ currentProduct.product_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家姓名">{{ currentProduct.seller?.customer_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家手机">{{ currentProduct.seller?.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原商品ID">{{ currentProduct.original_product_id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原商品类型">
|
||||||
|
<el-tag :type="currentProduct.original_product_type === 1 ? 'primary' : 'warning'">
|
||||||
|
{{ currentProduct.original_product_type === 1 ? '一级商品' : '二级商品' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="成本价">¥{{ formatAmount(currentProduct.cost_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="售价">¥{{ formatAmount(currentProduct.selling_price) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="利润">
|
||||||
|
<span :class="getProfitClass(currentProduct.selling_price - currentProduct.cost_price)">
|
||||||
|
¥{{ formatAmount(currentProduct.selling_price - currentProduct.cost_price) }}
|
||||||
|
</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数量">{{ currentProduct.quantity }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="浏览量">{{ currentProduct.view_count }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="getStatusTagType(currentProduct.status)">
|
||||||
|
{{ getStatusName(currentProduct.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品描述" :span="2">{{ currentProduct.product_description || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDate(currentProduct.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDate(currentProduct.updated_at) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 卖家信息 -->
|
||||||
|
<div v-if="currentProduct.seller" class="mt-4">
|
||||||
|
<h4>卖家信息</h4>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="卖家ID">{{ currentProduct.seller.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="卖家身份码">{{ currentProduct.seller.identity_code }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="真实姓名">{{ currentProduct.seller.real_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前积分">{{ currentProduct.seller.current_points }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司名称" :span="2">{{ currentProduct.seller.company_name || '无' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<div v-if="getProductImages(currentProduct.product_images).length > 0" class="mt-4">
|
||||||
|
<h4>商品图片</h4>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="(image, index) in getProductImages(currentProduct.product_images)"
|
||||||
|
:key="index"
|
||||||
|
:src="getImageUrl(image)"
|
||||||
|
:preview-src-list="getProductImages(currentProduct.product_images).map(img => getImageUrl(img))"
|
||||||
|
fit="cover"
|
||||||
|
class="product-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const products = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
seller_id: '',
|
||||||
|
product_name: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前查看的商品
|
||||||
|
const currentProduct = ref({})
|
||||||
|
|
||||||
|
// 获取商品列表
|
||||||
|
const getProducts = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤空值
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await axios.get('/admin/products/secondary', { params })
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
products.value = response.data.data.list || []
|
||||||
|
pagination.total = response.data.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取二级商品列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
seller_id: '',
|
||||||
|
product_name: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态名称
|
||||||
|
const getStatusName = (status) => {
|
||||||
|
const statusMap = { 0: '下架', 1: '上架', 2: '已售出' }
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusTagType = (status) => {
|
||||||
|
const typeMap = { 0: 'danger', 1: 'success', 2: 'info' }
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取利润颜色类
|
||||||
|
const getProfitClass = (profit) => {
|
||||||
|
if (profit > 0) {
|
||||||
|
return 'text-green-600 font-semibold'
|
||||||
|
} else if (profit < 0) {
|
||||||
|
return 'text-red-600 font-semibold'
|
||||||
|
}
|
||||||
|
return 'text-gray-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${window.location.origin}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品图片列表
|
||||||
|
const getProductImages = (images) => {
|
||||||
|
if (!images) return []
|
||||||
|
return images.split(',').filter(img => img.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
if (!amount) return '0.00'
|
||||||
|
return Number(amount).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示商品详情
|
||||||
|
const showProductDetail = (product) => {
|
||||||
|
currentProduct.value = product
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getProducts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.secondary-products-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-desc {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-400 {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-600 {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-green-600 {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-600 {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#3B82F6' // 扩展主题色
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "localhost",
|
||||||
|
cors: true,
|
||||||
|
port: 1111,
|
||||||
|
open: false, //自动打开
|
||||||
|
proxy: {
|
||||||
|
// 这里的ccc可乱写, 是拼接在url后面的地址 如果接口中没有统一的后缀可自定义
|
||||||
|
// 如果有统一后缀, 如api, 直接写api即可, 也不用rewrite了
|
||||||
|
"^/back": {
|
||||||
|
target: "http://localhost:8090", // 真实接口地址, 后端给的基地址
|
||||||
|
changeOrigin: true, // 允许跨域
|
||||||
|
secure: false, //关键参数,不懂的自己去查
|
||||||
|
rewrite: (path) => path.replace(/^\/back/, '/back')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
module awesomeProject
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/gin-gonic/gin v1.10.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/mysql v1.6.0 // indirect
|
||||||
|
gorm.io/driver/postgres v1.6.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||||
|
gorm.io/gorm v1.30.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
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=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||||
|
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MineConfig *Config
|
||||||
|
|
||||||
|
// Config 应用配置结构
|
||||||
|
type Config struct {
|
||||||
|
App AppConfig `yaml:"app"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
JWT JWTConfig `yaml:"jwt"`
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
Log LogConfig `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppConfig 应用基础配置
|
||||||
|
type AppConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Env string `yaml:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseConfig 数据库配置
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Driver string `yaml:"driver"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
DBName string `yaml:"dbname"`
|
||||||
|
Charset string `yaml:"charset"`
|
||||||
|
SQLitePath string `yaml:"sqlite_path"`
|
||||||
|
MaxIdleConns int `yaml:"max_idle_conns"`
|
||||||
|
MaxOpenConns int `yaml:"max_open_conns"`
|
||||||
|
ConnMaxLifetime int `yaml:"conn_max_lifetime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTConfig JWT配置
|
||||||
|
type JWTConfig struct {
|
||||||
|
SecretKey string `yaml:"secret_key"`
|
||||||
|
ExpireHours int `yaml:"expire_hours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig 服务器配置
|
||||||
|
type ServerConfig struct {
|
||||||
|
ReadTimeout int `yaml:"read_timeout"`
|
||||||
|
WriteTimeout int `yaml:"write_timeout"`
|
||||||
|
StaticPath string `yaml:"static_path"`
|
||||||
|
UploadMaxSize int `yaml:"upload_max_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogConfig 日志配置
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
FilePath string `yaml:"file_path"`
|
||||||
|
MaxSize int `yaml:"max_size"`
|
||||||
|
MaxAge int `yaml:"max_age"`
|
||||||
|
MaxBackups int `yaml:"max_backups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig 加载配置文件
|
||||||
|
func LoadConfig(configPath string) (*Config, error) {
|
||||||
|
config := &Config{}
|
||||||
|
|
||||||
|
// 读取配置文件
|
||||||
|
file, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析YAML
|
||||||
|
err = yaml.Unmarshal(file, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置全局配置
|
||||||
|
MineConfig = config
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDSN 获取数据库连接字符串
|
||||||
|
func (db *DatabaseConfig) GetDSN() string {
|
||||||
|
switch db.Driver {
|
||||||
|
case "mysql":
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
||||||
|
db.Username, db.Password, db.Host, db.Port, db.DBName, db.Charset)
|
||||||
|
case "postgres":
|
||||||
|
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
||||||
|
db.Host, db.Username, db.Password, db.DBName, db.Port)
|
||||||
|
case "sqlite":
|
||||||
|
return db.SQLitePath
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
// InitDatabase 初始化数据库连接
|
||||||
|
func InitDatabase(config *DatabaseConfig) error {
|
||||||
|
var err error
|
||||||
|
var dialector gorm.Dialector
|
||||||
|
|
||||||
|
// 根据驱动类型选择相应的方言
|
||||||
|
switch config.Driver {
|
||||||
|
case "mysql":
|
||||||
|
dialector = mysql.Open(config.GetDSN())
|
||||||
|
case "postgres":
|
||||||
|
dialector = postgres.Open(config.GetDSN())
|
||||||
|
case "sqlite":
|
||||||
|
dialector = sqlite.Open(config.GetDSN())
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("不支持的数据库驱动: %s", config.Driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GORM 配置
|
||||||
|
gormConfig := &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
DB, err = gorm.Open(dialector, gormConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接数据库失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取底层的 *sql.DB
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取数据库连接失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接池参数
|
||||||
|
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
|
||||||
|
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second)
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
if err := sqlDB.Ping(); err != nil {
|
||||||
|
return fmt.Errorf("数据库连接测试失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("数据库连接成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB 获取数据库实例
|
||||||
|
func GetDB() *gorm.DB {
|
||||||
|
return DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseDatabase 关闭数据库连接
|
||||||
|
func CloseDatabase() error {
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoMigrate 自动迁移数据表
|
||||||
|
func AutoMigrate(models ...interface{}) error {
|
||||||
|
return DB.AutoMigrate(models...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Success(data interface{}) Result {
|
||||||
|
|
||||||
|
return Result{200, true, "", data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuccessWithMessage(data interface{}, message string) Result {
|
||||||
|
return Result{200, true, message, data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(code int, message string) Result {
|
||||||
|
return Result{code, false, message, nil}
|
||||||
|
}
|
||||||
|
func ErrorWithData(data interface{}, code int, message string) Result {
|
||||||
|
return Result{code, true, message, data}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// 配置键常量定义
|
||||||
|
const (
|
||||||
|
// 系统基础配置
|
||||||
|
ConfigSiteName = "site_name" // 网站名称
|
||||||
|
ConfigSiteUrl = "site_url" // 网站地址
|
||||||
|
ConfigSiteLogo = "site_logo" // 网站Logo
|
||||||
|
ConfigSiteDescription = "site_description" // 网站描述
|
||||||
|
|
||||||
|
// 商品相关配置
|
||||||
|
ConfigPriceIncreaseRate = "warehouse_price_rate" // 商品入库价格增长率
|
||||||
|
ConfigMaxStockQuantity = "max_stock_quantity" // 最大库存数量
|
||||||
|
ConfigMinSalePrice = "min_sale_price" // 最低销售价格
|
||||||
|
|
||||||
|
// 用户相关配置
|
||||||
|
ConfigMaxUserLevel = "max_user_level" // 用户最大等级
|
||||||
|
ConfigDefaultUserScore = "default_user_score" // 新用户默认积分
|
||||||
|
ConfigReferralBonus = "referral_bonus" // 推荐奖励积分
|
||||||
|
|
||||||
|
// 订单相关配置
|
||||||
|
ConfigOrderTimeout = "order_timeout" // 订单超时时间(分钟)
|
||||||
|
ConfigMaxOrderQuantity = "max_order_quantity" // 单笔订单最大数量
|
||||||
|
ConfigMinOrderAmount = "min_order_amount" // 最小订单金额
|
||||||
|
|
||||||
|
// 系统功能开关
|
||||||
|
ConfigEnableRegistration = "enable_registration" // 是否开放注册
|
||||||
|
ConfigEnableUpgrade = "enable_upgrade" // 是否开启升级功能
|
||||||
|
ConfigMaintenanceMode = "maintenance_mode" // 维护模式
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// ConfigEvent 配置变更事件
|
||||||
|
type ConfigEvent struct {
|
||||||
|
ConfigKey string `json:"config_key"` // 配置键
|
||||||
|
OldValue interface{} `json:"old_value"` // 旧值
|
||||||
|
NewValue interface{} `json:"new_value"` // 新值
|
||||||
|
ChangeType string `json:"change_type"` // 变更类型:create, update, delete
|
||||||
|
ChangedBy uint `json:"changed_by"` // 修改人ID
|
||||||
|
ChangedAt int64 `json:"changed_at"` // 修改时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigEventHandler 配置事件处理器接口
|
||||||
|
type ConfigEventHandler interface {
|
||||||
|
Handle(event ConfigEvent) error
|
||||||
|
GetConfigKeys() []string // 返回该处理器关心的配置键列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigChange 配置变更记录(用于审计)
|
||||||
|
type ConfigChange struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
ConfigKey string `gorm:"type:varchar(100);not null;comment:配置键" json:"config_key"`
|
||||||
|
OldValue string `gorm:"type:text;comment:旧值" json:"old_value"`
|
||||||
|
NewValue string `gorm:"type:text;comment:新值" json:"new_value"`
|
||||||
|
ChangeType string `gorm:"type:varchar(20);not null;comment:变更类型" json:"change_type"`
|
||||||
|
ChangedBy uint `gorm:"type:int;not null;comment:修改人ID" json:"changed_by"`
|
||||||
|
ChangedAt int64 `gorm:"type:bigint;not null;comment:修改时间戳" json:"changed_at"`
|
||||||
|
Extra map[string]interface{} `gorm:"type:json;comment:额外信息" json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ConfigChange) TableName() string {
|
||||||
|
return "config_changes"
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/config"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigEventManager 配置事件管理器
|
||||||
|
type ConfigEventManager struct {
|
||||||
|
handlers map[string][]config.ConfigEventHandler // key: config_key, value: handlers
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
configEventManager *ConfigEventManager
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetConfigEventManager 获取配置事件管理器单例
|
||||||
|
func GetConfigEventManager() *ConfigEventManager {
|
||||||
|
once.Do(func() {
|
||||||
|
configEventManager = &ConfigEventManager{
|
||||||
|
handlers: make(map[string][]config.ConfigEventHandler),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return configEventManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandler 注册配置事件处理器
|
||||||
|
func (cem *ConfigEventManager) RegisterHandler(handler config.ConfigEventHandler) {
|
||||||
|
cem.mutex.Lock()
|
||||||
|
defer cem.mutex.Unlock()
|
||||||
|
|
||||||
|
configKeys := handler.GetConfigKeys()
|
||||||
|
for _, key := range configKeys {
|
||||||
|
if cem.handlers[key] == nil {
|
||||||
|
cem.handlers[key] = []config.ConfigEventHandler{}
|
||||||
|
}
|
||||||
|
cem.handlers[key] = append(cem.handlers[key], handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("注册配置事件处理器,监听配置: %v", configKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerEvent 触发配置变更事件
|
||||||
|
func (cem *ConfigEventManager) TriggerEvent(event config.ConfigEvent) {
|
||||||
|
cem.mutex.RLock()
|
||||||
|
handlers := cem.handlers[event.ConfigKey]
|
||||||
|
cem.mutex.RUnlock()
|
||||||
|
|
||||||
|
if len(handlers) == 0 {
|
||||||
|
log.Printf("配置 %s 无对应的事件处理器", event.ConfigKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("触发配置变更事件: %s, 旧值: %v, 新值: %v", event.ConfigKey, event.OldValue, event.NewValue)
|
||||||
|
|
||||||
|
// 并发处理事件,避免阻塞
|
||||||
|
for _, handler := range handlers {
|
||||||
|
go func(h config.ConfigEventHandler) {
|
||||||
|
if err := h.Handle(event); err != nil {
|
||||||
|
log.Printf("处理配置事件失败: %s, 错误: %v", event.ConfigKey, err)
|
||||||
|
}
|
||||||
|
}(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRegisteredHandlers 列出已注册的处理器(用于调试)
|
||||||
|
func (cem *ConfigEventManager) ListRegisteredHandlers() map[string]int {
|
||||||
|
cem.mutex.RLock()
|
||||||
|
defer cem.mutex.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]int)
|
||||||
|
for key, handlers := range cem.handlers {
|
||||||
|
result[key] = len(handlers)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/config"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PriceConfigHandler 价格配置变更处理器
|
||||||
|
type PriceConfigHandler struct{}
|
||||||
|
|
||||||
|
// GetConfigKeys 返回该处理器关心的配置键
|
||||||
|
func (h *PriceConfigHandler) GetConfigKeys() []string {
|
||||||
|
return []string{
|
||||||
|
config.ConfigPriceIncreaseRate,
|
||||||
|
config.ConfigMinSalePrice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 处理配置变更事件
|
||||||
|
func (h *PriceConfigHandler) Handle(event config.ConfigEvent) error {
|
||||||
|
switch event.ConfigKey {
|
||||||
|
case config.ConfigPriceIncreaseRate:
|
||||||
|
return h.handlePriceIncreaseRateChange(event)
|
||||||
|
case config.ConfigMinSalePrice:
|
||||||
|
return h.handleMinSalePriceChange(event)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知的配置键: %s", event.ConfigKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePriceIncreaseRateChange 处理价格增长率变更
|
||||||
|
func (h *PriceConfigHandler) handlePriceIncreaseRateChange(event config.ConfigEvent) error {
|
||||||
|
log.Printf("价格增长率配置变更: %v -> %v", event.OldValue, event.NewValue)
|
||||||
|
|
||||||
|
// 解析新的增长率
|
||||||
|
newRateStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("价格增长率配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
newRate, err := strconv.ParseFloat(newRateStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("价格增长率配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
var secondList []models.SecondaryProduct
|
||||||
|
result := db.Where("status = ? AND quantity > ?", 1, 0).Find(&secondList) // 修正:传递切片指针而不是值
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Printf("查询失败: %v", result.Error)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到符合条件的记录,直接返回
|
||||||
|
if len(secondList) == 0 {
|
||||||
|
log.Printf("没有找到需要更新的二级商品")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
for _, product := range secondList {
|
||||||
|
// 检查商品是否有关联的仓库记录
|
||||||
|
if product.UserWarehouseID == 0 {
|
||||||
|
log.Printf("二级商品 ID: %d 没有关联的仓库记录,跳过", product.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var warehouse models.UserWarehouse
|
||||||
|
if err := tx.Where("id = ?", product.UserWarehouseID).First(&warehouse).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("查询仓库记录失败,ID: %d, 错误: %v", product.UserWarehouseID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newWarehousePrice := warehouse.PurchasePrice + newRate
|
||||||
|
if err := tx.Model(&models.UserWarehouse{}).Where("id = ?", product.UserWarehouseID).
|
||||||
|
Update("quantity", product.Quantity+warehouse.Quantity).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("更新仓库价格失败,ID: %d, 错误: %v", product.UserWarehouseID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("已更新仓库商品 ID: %d 的价格,新价格: %.2f", product.UserWarehouseID, newWarehousePrice)
|
||||||
|
|
||||||
|
tx.Model(&models.SecondaryProduct{}).Where("id = ?", product.ID).Update("status", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Model(&models.UserWarehouse{}).Where("quantity>?", 0).Update("status", 1).
|
||||||
|
Update("warehouse_price", gorm.Expr("purchase_price + ?", newRate))
|
||||||
|
tx.Commit()
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("价格增长率已更新为: %.2f%%", newRate)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMinSalePriceChange 处理最低销售价格变更
|
||||||
|
func (h *PriceConfigHandler) handleMinSalePriceChange(event config.ConfigEvent) error {
|
||||||
|
log.Printf("最低销售价格配置变更: %v -> %v", event.OldValue, event.NewValue)
|
||||||
|
|
||||||
|
// 解析新的最低价格
|
||||||
|
newPriceStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("最低销售价格配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
newPrice, err := strconv.ParseFloat(newPriceStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("最低销售价格配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证价格范围
|
||||||
|
if newPrice < 0 {
|
||||||
|
return fmt.Errorf("最低销售价格不能为负数")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务逻辑:检查并更新现有的低于最低价格的商品
|
||||||
|
if err := h.updateLowPriceProducts(newPrice); err != nil {
|
||||||
|
log.Printf("更新低价商品失败: %v", err)
|
||||||
|
// 不返回错误,避免阻塞其他处理器
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("最低销售价格已更新为: %.2f", newPrice)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateLowPriceProducts 更新低于最低价格的商品
|
||||||
|
func (h *PriceConfigHandler) updateLowPriceProducts(minPrice float64) error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 查找低于最低价格的二级商品
|
||||||
|
var lowPriceProducts []models.SecondaryProduct
|
||||||
|
err := db.Where("sale_price < ? AND status = 1", minPrice).Find(&lowPriceProducts).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询低价商品失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lowPriceProducts) == 0 {
|
||||||
|
log.Printf("没有发现低于最低价格(%.2f)的商品", minPrice)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新这些商品的状态为下架
|
||||||
|
result := db.Model(&models.SecondaryProduct{}).
|
||||||
|
Where("sale_price < ? AND status = 1", minPrice).
|
||||||
|
Update("status", 0)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("批量下架低价商品失败: %v", result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("已下架 %d 个低于最低价格的商品", result.RowsAffected)
|
||||||
|
|
||||||
|
// 这里可以添加更多逻辑,比如:
|
||||||
|
// 1. 发送通知给商品卖家
|
||||||
|
// 2. 记录下架日志
|
||||||
|
// 3. 更新相关统计数据
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/config"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SystemConfigHandler 系统配置变更处理器
|
||||||
|
type SystemConfigHandler struct{}
|
||||||
|
|
||||||
|
// GetConfigKeys 返回该处理器关心的配置键
|
||||||
|
func (h *SystemConfigHandler) GetConfigKeys() []string {
|
||||||
|
return []string{
|
||||||
|
config.ConfigEnableRegistration,
|
||||||
|
config.ConfigMaintenanceMode,
|
||||||
|
config.ConfigSiteName,
|
||||||
|
config.ConfigSiteUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 处理配置变更事件
|
||||||
|
func (h *SystemConfigHandler) Handle(event config.ConfigEvent) error {
|
||||||
|
switch event.ConfigKey {
|
||||||
|
case config.ConfigEnableRegistration:
|
||||||
|
return h.handleRegistrationToggle(event)
|
||||||
|
case config.ConfigMaintenanceMode:
|
||||||
|
return h.handleMaintenanceModeToggle(event)
|
||||||
|
case config.ConfigSiteName:
|
||||||
|
return h.handleSiteNameChange(event)
|
||||||
|
case config.ConfigSiteUrl:
|
||||||
|
return h.handleSiteUrlChange(event)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知的配置键: %s", event.ConfigKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRegistrationToggle 处理注册开关变更
|
||||||
|
func (h *SystemConfigHandler) handleRegistrationToggle(event config.ConfigEvent) error {
|
||||||
|
newValueStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("注册开关配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, err := strconv.ParseBool(newValueStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("注册开关配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
log.Printf("用户注册已开启")
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 清理注册限制缓存
|
||||||
|
// 2. 发送系统通知
|
||||||
|
// 3. 更新前端配置缓存
|
||||||
|
} else {
|
||||||
|
log.Printf("用户注册已关闭")
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 设置注册限制
|
||||||
|
// 2. 通知管理员
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMaintenanceModeToggle 处理维护模式开关
|
||||||
|
func (h *SystemConfigHandler) handleMaintenanceModeToggle(event config.ConfigEvent) error {
|
||||||
|
newValueStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("维护模式配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, err := strconv.ParseBool(newValueStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("维护模式配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
log.Printf("系统进入维护模式")
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 断开非管理员用户连接
|
||||||
|
// 2. 停止定时任务
|
||||||
|
// 3. 发送维护通知
|
||||||
|
// 4. 设置维护页面
|
||||||
|
} else {
|
||||||
|
log.Printf("系统退出维护模式")
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 恢复正常服务
|
||||||
|
// 2. 重启定时任务
|
||||||
|
// 3. 发送恢复通知
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSiteNameChange 处理网站名称变更
|
||||||
|
func (h *SystemConfigHandler) handleSiteNameChange(event config.ConfigEvent) error {
|
||||||
|
newName, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("网站名称配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newName) == 0 {
|
||||||
|
return fmt.Errorf("网站名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("网站名称已更新: %s -> %s", event.OldValue, newName)
|
||||||
|
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 更新邮件模板中的网站名称
|
||||||
|
// 2. 刷新前端缓存
|
||||||
|
// 3. 更新SEO相关配置
|
||||||
|
// 4. 发送更新通知
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSiteUrlChange 处理网站地址变更
|
||||||
|
func (h *SystemConfigHandler) handleSiteUrlChange(event config.ConfigEvent) error {
|
||||||
|
newUrl, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("网站地址配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newUrl) == 0 {
|
||||||
|
return fmt.Errorf("网站地址不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的URL格式验证
|
||||||
|
if len(newUrl) < 7 || (newUrl[:7] != "http://" && newUrl[:8] != "https://") {
|
||||||
|
return fmt.Errorf("网站地址格式不正确,必须以http://或https://开头")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("网站地址已更新: %s -> %s", event.OldValue, newUrl)
|
||||||
|
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 更新CORS配置
|
||||||
|
// 2. 更新回调URL
|
||||||
|
// 3. 刷新API文档
|
||||||
|
// 4. 更新第三方服务配置
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/config"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserConfigHandler 用户配置变更处理器
|
||||||
|
type UserConfigHandler struct{}
|
||||||
|
|
||||||
|
// GetConfigKeys 返回该处理器关心的配置键
|
||||||
|
func (h *UserConfigHandler) GetConfigKeys() []string {
|
||||||
|
return []string{
|
||||||
|
config.ConfigDefaultUserScore,
|
||||||
|
config.ConfigReferralBonus,
|
||||||
|
config.ConfigMaxUserLevel,
|
||||||
|
config.ConfigEnableRegistration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 处理配置变更事件
|
||||||
|
func (h *UserConfigHandler) Handle(event config.ConfigEvent) error {
|
||||||
|
switch event.ConfigKey {
|
||||||
|
case config.ConfigDefaultUserScore:
|
||||||
|
return h.handleDefaultUserScoreChange(event)
|
||||||
|
case config.ConfigReferralBonus:
|
||||||
|
return h.handleReferralBonusChange(event)
|
||||||
|
case config.ConfigMaxUserLevel:
|
||||||
|
return h.handleMaxUserLevelChange(event)
|
||||||
|
case config.ConfigEnableRegistration:
|
||||||
|
return h.handleRegistrationToggle(event)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知的配置键: %s", event.ConfigKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDefaultUserScoreChange 处理默认用户积分变更
|
||||||
|
func (h *UserConfigHandler) handleDefaultUserScoreChange(event config.ConfigEvent) error {
|
||||||
|
newScoreStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("默认用户积分配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
newScore, err := strconv.ParseFloat(newScoreStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("默认用户积分配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newScore < 0 {
|
||||||
|
return fmt.Errorf("默认用户积分不能为负数")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("默认用户积分已更新: %v -> %.2f", event.OldValue, newScore)
|
||||||
|
|
||||||
|
// 这里可以添加业务逻辑,比如:
|
||||||
|
// 1. 更新新注册用户的积分计算逻辑
|
||||||
|
// 2. 发送通知给管理员
|
||||||
|
// 3. 更新缓存中的默认积分值
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleReferralBonusChange 处理推荐奖励变更
|
||||||
|
func (h *UserConfigHandler) handleReferralBonusChange(event config.ConfigEvent) error {
|
||||||
|
newBonusStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("推荐奖励配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
newBonus, err := strconv.ParseFloat(newBonusStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("推荐奖励配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newBonus < 0 {
|
||||||
|
return fmt.Errorf("推荐奖励不能为负数")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("推荐奖励已更新: %v -> %.2f", event.OldValue, newBonus)
|
||||||
|
|
||||||
|
// 这里可以添加业务逻辑,比如:
|
||||||
|
// 1. 重新计算推荐奖励策略
|
||||||
|
// 2. 发送通知给有推荐关系的用户
|
||||||
|
// 3. 更新推荐系统的计算规则
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMaxUserLevelChange 处理最大用户等级变更
|
||||||
|
func (h *UserConfigHandler) handleMaxUserLevelChange(event config.ConfigEvent) error {
|
||||||
|
newLevelStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("最大用户等级配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
newLevel, err := strconv.Atoi(newLevelStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("最大用户等级配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newLevel < 1 {
|
||||||
|
return fmt.Errorf("最大用户等级不能小于1")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("最大用户等级已更新: %v -> %d", event.OldValue, newLevel)
|
||||||
|
|
||||||
|
// 检查是否有用户等级超过新的最大等级
|
||||||
|
if err := h.checkAndAdjustUserLevels(newLevel); err != nil {
|
||||||
|
log.Printf("调整用户等级失败: %v", err)
|
||||||
|
// 不返回错误,避免阻塞其他处理器
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRegistrationToggle 处理注册开关变更
|
||||||
|
func (h *UserConfigHandler) handleRegistrationToggle(event config.ConfigEvent) error {
|
||||||
|
newValueStr, ok := event.NewValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("注册开关配置值类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled, err := strconv.ParseBool(newValueStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("注册开关配置值格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
log.Printf("用户注册已开启")
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 清理注册限制缓存
|
||||||
|
// 2. 发送开放注册通知
|
||||||
|
// 3. 更新前端注册页面状态
|
||||||
|
} else {
|
||||||
|
log.Printf("用户注册已关闭")
|
||||||
|
// 这里可以添加逻辑,比如:
|
||||||
|
// 1. 设置注册限制
|
||||||
|
// 2. 显示注册关闭页面
|
||||||
|
// 3. 通知相关管理员
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndAdjustUserLevels 检查并调整超过最大等级的用户
|
||||||
|
func (h *UserConfigHandler) checkAndAdjustUserLevels(maxLevel int) error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 查找等级超过最大等级的用户
|
||||||
|
var highLevelUsers []models.User
|
||||||
|
err := db.Where("user_level > ?", maxLevel).Find(&highLevelUsers).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询高等级用户失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(highLevelUsers) == 0 {
|
||||||
|
log.Printf("没有发现超过最大等级(%d)的用户", maxLevel)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量调整这些用户的等级
|
||||||
|
result := db.Model(&models.User{}).
|
||||||
|
Where("user_level > ?", maxLevel).
|
||||||
|
Update("user_level", maxLevel)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("批量调整用户等级失败: %v", result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("已调整 %d 个用户的等级到最大等级: %d", result.RowsAffected, maxLevel)
|
||||||
|
|
||||||
|
// 这里可以添加更多逻辑,比如:
|
||||||
|
// 1. 发送等级调整通知给用户
|
||||||
|
// 2. 记录等级变更日志
|
||||||
|
// 3. 重新计算相关权益
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/events/handlers"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitEventSystem 初始化事件系统
|
||||||
|
func InitEventSystem() {
|
||||||
|
log.Println("初始化配置事件系统...")
|
||||||
|
|
||||||
|
eventManager := GetConfigEventManager()
|
||||||
|
|
||||||
|
// 注册价格配置处理器
|
||||||
|
priceHandler := &handlers.PriceConfigHandler{}
|
||||||
|
eventManager.RegisterHandler(priceHandler)
|
||||||
|
|
||||||
|
// 注册系统配置处理器
|
||||||
|
systemHandler := &handlers.SystemConfigHandler{}
|
||||||
|
eventManager.RegisterHandler(systemHandler)
|
||||||
|
|
||||||
|
// 注册用户配置处理器
|
||||||
|
userHandler := &handlers.UserConfigHandler{}
|
||||||
|
eventManager.RegisterHandler(userHandler)
|
||||||
|
|
||||||
|
// 可以继续注册更多处理器...
|
||||||
|
// 例如:
|
||||||
|
// orderHandler := &handlers.OrderConfigHandler{}
|
||||||
|
// eventManager.RegisterHandler(orderHandler)
|
||||||
|
|
||||||
|
log.Printf("配置事件系统初始化完成,已注册处理器数量: %v", eventManager.ListRegisteredHandlers())
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminBannerHandler 管理端轮播图处理器
|
||||||
|
type AdminBannerHandler struct{}
|
||||||
|
|
||||||
|
// CreateBannerRequest 创建轮播图请求结构体
|
||||||
|
type CreateBannerRequest struct {
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
ImageUrl string `json:"image_url" binding:"required"`
|
||||||
|
LinkUrl string `json:"link_url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBannerRequest 更新轮播图请求结构体
|
||||||
|
type UpdateBannerRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ImageUrl string `json:"image_url"`
|
||||||
|
LinkUrl string `json:"link_url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBanners 获取轮播图列表
|
||||||
|
func (h *AdminBannerHandler) GetBanners(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以查看轮播图管理
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var banners []models.Banner
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
db.Model(&models.Banner{}).Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := db.Offset(offset).Limit(pageSize).Order("sort_order ASC, created_at DESC").Find(&banners).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询轮播图失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": banners,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBanner 获取单个轮播图
|
||||||
|
func (h *AdminBannerHandler) GetBanner(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以查看轮播图详情
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bannerID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "轮播图ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var banner models.Banner
|
||||||
|
if err := common.GetDB().First(&banner, uint(bannerID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "轮播图不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(banner))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBanner 创建轮播图
|
||||||
|
func (h *AdminBannerHandler) CreateBanner(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以创建轮播图
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateBannerRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建轮播图对象
|
||||||
|
newBanner := models.Banner{
|
||||||
|
Title: req.Title,
|
||||||
|
ImageUrl: req.ImageUrl,
|
||||||
|
LinkUrl: req.LinkUrl,
|
||||||
|
Description: req.Description,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
Status: req.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存轮播图
|
||||||
|
if err := common.GetDB().Create(&newBanner).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建轮播图失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(newBanner))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBanner 更新轮播图
|
||||||
|
func (h *AdminBannerHandler) UpdateBanner(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新轮播图
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bannerID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "轮播图ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateBannerRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查轮播图是否存在
|
||||||
|
var existBanner models.Banner
|
||||||
|
if err := common.GetDB().First(&existBanner, uint(bannerID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "轮播图不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备更新数据
|
||||||
|
updateData := models.Banner{
|
||||||
|
Title: req.Title,
|
||||||
|
ImageUrl: req.ImageUrl,
|
||||||
|
LinkUrl: req.LinkUrl,
|
||||||
|
Description: req.Description,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
Status: req.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新轮播图信息
|
||||||
|
updateData.ID = uint(bannerID)
|
||||||
|
if err := common.GetDB().Model(&existBanner).Updates(&updateData).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新轮播图失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的轮播图信息
|
||||||
|
var updatedBanner models.Banner
|
||||||
|
common.GetDB().First(&updatedBanner, uint(bannerID))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedBanner))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBanner 删除轮播图
|
||||||
|
func (h *AdminBannerHandler) DeleteBanner(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以删除轮播图
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bannerID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "轮播图ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查轮播图是否存在
|
||||||
|
var banner models.Banner
|
||||||
|
if err := common.GetDB().First(&banner, uint(bannerID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "轮播图不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除轮播图
|
||||||
|
if err := common.GetDB().Delete(&banner).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除轮播图失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success("轮播图删除成功"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/config"
|
||||||
|
"awesomeProject/internal/events"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminConfigHandler 管理端系统配置处理器
|
||||||
|
type AdminConfigHandler struct{}
|
||||||
|
|
||||||
|
// GetSystemConfigs 获取系统配置列表
|
||||||
|
func (h *AdminConfigHandler) GetSystemConfigs(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以查看系统配置
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
configKey := c.Query("config_key")
|
||||||
|
configType := c.Query("config_type")
|
||||||
|
isSystem := c.Query("is_system")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var configs []models.SystemConfig
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.SystemConfig{})
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if configKey != "" {
|
||||||
|
query = query.Where("config_key LIKE ?", "%"+configKey+"%")
|
||||||
|
}
|
||||||
|
if configType != "" {
|
||||||
|
query = query.Where("config_type = ?", configType)
|
||||||
|
}
|
||||||
|
if isSystem != "" {
|
||||||
|
query = query.Where("is_system = ?", isSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("is_system DESC, created_at DESC").Find(&configs).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询系统配置失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": configs,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemConfig 获取单个系统配置
|
||||||
|
func (h *AdminConfigHandler) GetSystemConfig(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以查看系统配置
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "配置ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config models.SystemConfig
|
||||||
|
err = common.GetDB().First(&config, uint(configID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "系统配置不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询系统配置失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSystemConfig 创建系统配置
|
||||||
|
func (h *AdminConfigHandler) CreateSystemConfig(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以创建系统配置
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newConfig models.SystemConfig
|
||||||
|
if err := c.ShouldBindJSON(&newConfig); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置键是否已存在
|
||||||
|
var existConfig models.SystemConfig
|
||||||
|
if err := common.GetDB().Where("config_key = ?", newConfig.ConfigKey).First(&existConfig).Error; err == nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "配置键已存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
if err := common.GetDB().Create(&newConfig).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建系统配置失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(newConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSystemConfig 更新系统配置
|
||||||
|
func (h *AdminConfigHandler) UpdateSystemConfig(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新系统配置
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "配置ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData models.SystemConfig
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置是否存在
|
||||||
|
var existConfig models.SystemConfig
|
||||||
|
if err := common.GetDB().First(&existConfig, uint(configID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "系统配置不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是系统配置,不允许修改配置键
|
||||||
|
if existConfig.IsSystem == 1 && updateData.ConfigKey != existConfig.ConfigKey {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "系统配置不允许修改配置键"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果修改了配置键,检查是否已存在
|
||||||
|
if updateData.ConfigKey != existConfig.ConfigKey {
|
||||||
|
var duplicateConfig models.SystemConfig
|
||||||
|
if err := common.GetDB().Where("config_key = ? AND id != ?", updateData.ConfigKey, configID).First(&duplicateConfig).Error; err == nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "配置键已存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录旧值,用于事件触发
|
||||||
|
oldValue := existConfig.ConfigValue
|
||||||
|
|
||||||
|
// 更新配置信息
|
||||||
|
updateData.ID = uint(configID)
|
||||||
|
if err := common.GetDB().Model(&existConfig).Updates(&updateData).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新系统配置失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的配置信息
|
||||||
|
var updatedConfig models.SystemConfig
|
||||||
|
common.GetDB().First(&updatedConfig, uint(configID))
|
||||||
|
|
||||||
|
// 如果配置值发生变化,触发配置变更事件
|
||||||
|
if oldValue != updatedConfig.ConfigValue {
|
||||||
|
configEvent := config.ConfigEvent{
|
||||||
|
ConfigKey: updatedConfig.ConfigKey,
|
||||||
|
OldValue: oldValue,
|
||||||
|
NewValue: updatedConfig.ConfigValue,
|
||||||
|
ChangeType: "update",
|
||||||
|
ChangedBy: user.ID,
|
||||||
|
ChangedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步触发事件,避免阻塞响应
|
||||||
|
go func() {
|
||||||
|
events.GetConfigEventManager().TriggerEvent(configEvent)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 记录配置变更日志(可选)
|
||||||
|
go h.logConfigChange(configEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSystemConfig 删除系统配置
|
||||||
|
func (h *AdminConfigHandler) DeleteSystemConfig(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以删除系统配置
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "配置ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置是否存在
|
||||||
|
var existConfig models.SystemConfig
|
||||||
|
if err := common.GetDB().First(&existConfig, uint(configID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "系统配置不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统配置不允许删除
|
||||||
|
if existConfig.IsSystem == 1 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "系统配置不允许删除"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置
|
||||||
|
if err := common.GetDB().Delete(&existConfig).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除系统配置失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigByKey 根据配置键获取配置值(公共方法)
|
||||||
|
func (h *AdminConfigHandler) GetConfigByKey(c *gin.Context) {
|
||||||
|
configKey := c.Param("key")
|
||||||
|
if configKey == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "配置键不能为空"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config models.SystemConfig
|
||||||
|
err := common.GetDB().Where("config_key = ?", configKey).First(&config).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "配置不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询配置失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitSystemConfigs 初始化系统配置
|
||||||
|
func (h *AdminConfigHandler) InitSystemConfigs() error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 检查是否已有系统配置
|
||||||
|
var count int64
|
||||||
|
db.Model(&models.SystemConfig{}).Where("is_system = 1").Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
return nil // 已有系统配置,不重复创建
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认系统配置
|
||||||
|
defaultConfigs := []models.SystemConfig{
|
||||||
|
{
|
||||||
|
ConfigKey: "warehouse_price_rate",
|
||||||
|
ConfigValue: "1.10",
|
||||||
|
ConfigName: "入库价格增值率",
|
||||||
|
ConfigDescription: "商品进入用户仓库时的价格增值比例",
|
||||||
|
ConfigType: "number",
|
||||||
|
IsSystem: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigKey: "shipping_fee_rate",
|
||||||
|
ConfigValue: "0.05",
|
||||||
|
ConfigName: "运费比例",
|
||||||
|
ConfigDescription: "基于商品价格计算的运费比例",
|
||||||
|
ConfigType: "number",
|
||||||
|
IsSystem: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigKey: "platform_commission_rate",
|
||||||
|
ConfigValue: "0.03",
|
||||||
|
ConfigName: "平台佣金比例",
|
||||||
|
ConfigDescription: "平台从交易中收取的佣金比例",
|
||||||
|
ConfigType: "number",
|
||||||
|
IsSystem: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigKey: "min_withdraw_amount",
|
||||||
|
ConfigValue: "100.00",
|
||||||
|
ConfigName: "最小提现金额",
|
||||||
|
ConfigDescription: "用户最小提现金额限制",
|
||||||
|
ConfigType: "number",
|
||||||
|
IsSystem: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigKey: "max_product_images",
|
||||||
|
ConfigValue: "9",
|
||||||
|
ConfigName: "商品最大图片数量",
|
||||||
|
ConfigDescription: "单个商品最多可上传的图片数量",
|
||||||
|
ConfigType: "number",
|
||||||
|
IsSystem: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, config := range defaultConfigs {
|
||||||
|
if err := db.Create(&config).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// logConfigChange 记录配置变更日志
|
||||||
|
func (h *AdminConfigHandler) logConfigChange(event config.ConfigEvent) {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 创建配置变更记录
|
||||||
|
configChange := config.ConfigChange{
|
||||||
|
ConfigKey: event.ConfigKey,
|
||||||
|
OldValue: convertToString(event.OldValue),
|
||||||
|
NewValue: convertToString(event.NewValue),
|
||||||
|
ChangeType: event.ChangeType,
|
||||||
|
ChangedBy: event.ChangedBy,
|
||||||
|
ChangedAt: event.ChangedAt,
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"ip": "", // 可以从context中获取
|
||||||
|
"user_agent": "", // 可以从context中获取
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
if err := db.Create(&configChange).Error; err != nil {
|
||||||
|
// 记录日志失败不应该影响主流程,只记录错误日志
|
||||||
|
// common.Logger.Printf("记录配置变更日志失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToString 将interface{}转换为字符串
|
||||||
|
func convertToString(value interface{}) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,631 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminOrderHandler 管理端订单处理器
|
||||||
|
type AdminOrderHandler struct{}
|
||||||
|
|
||||||
|
// GetPurchaseOrders 获取买单列表
|
||||||
|
func (h *AdminOrderHandler) GetPurchaseOrders(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
orderNo := c.Query("order_no")
|
||||||
|
orderStatus := c.Query("order_status")
|
||||||
|
productType := c.Query("product_type")
|
||||||
|
sellerName := c.Query("seller_name")
|
||||||
|
buyerName := c.Query("buyer_name")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var orders []models.PurchaseOrder
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.PurchaseOrder{}).Preload("Buyer").Preload("Seller").Preload("Address")
|
||||||
|
|
||||||
|
// 如果是代理商,只能查看自己邀请的用户的订单
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 子查询:获取代理商邀请的用户ID列表
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
query = query.Where("buyer_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if orderNo != "" {
|
||||||
|
query = query.Where("order_no LIKE ?", "%"+orderNo+"%")
|
||||||
|
}
|
||||||
|
if orderStatus != "" {
|
||||||
|
query = query.Where("order_status = ?", orderStatus)
|
||||||
|
}
|
||||||
|
if productType != "" {
|
||||||
|
query = query.Where("product_type = ?", productType)
|
||||||
|
}
|
||||||
|
if sellerName != "" {
|
||||||
|
query = query.Joins("LEFT JOIN users AS sellers ON purchase_orders.seller_id = sellers.id").
|
||||||
|
Where("sellers.real_name LIKE ? OR sellers.phone LIKE ?", "%"+sellerName+"%", "%"+sellerName+"%")
|
||||||
|
}
|
||||||
|
if buyerName != "" {
|
||||||
|
query = query.Joins("LEFT JOIN users AS buyers ON purchase_orders.buyer_id = buyers.id").
|
||||||
|
Where("buyers.real_name LIKE ? OR buyers.phone LIKE ?", "%"+buyerName+"%", "%"+buyerName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&orders).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询买单列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": orders,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSalesOrders 获取卖单列表
|
||||||
|
func (h *AdminOrderHandler) GetSalesOrders(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
orderNo := c.Query("order_no")
|
||||||
|
orderStatus := c.Query("order_status")
|
||||||
|
sellerName := c.Query("seller_name")
|
||||||
|
buyerName := c.Query("buyer_name")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var orders []models.SalesOrder
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.SalesOrder{}).Preload("Seller").Preload("Buyer").Preload("SecondaryProduct").Preload("Address")
|
||||||
|
|
||||||
|
// 如果是代理商,只能查看自己邀请的用户相关的订单
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 子查询:获取代理商邀请的用户ID列表
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
query = query.Where("buyer_id IN (?) OR seller_id IN (?)", subQuery, subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if orderNo != "" {
|
||||||
|
query = query.Where("order_no LIKE ?", "%"+orderNo+"%")
|
||||||
|
}
|
||||||
|
if orderStatus != "" {
|
||||||
|
query = query.Where("order_status = ?", orderStatus)
|
||||||
|
}
|
||||||
|
if sellerName != "" {
|
||||||
|
query = query.Joins("LEFT JOIN users AS sellers ON sales_orders.seller_id = sellers.id").
|
||||||
|
Where("sellers.real_name LIKE ? OR sellers.phone LIKE ?", "%"+sellerName+"%", "%"+sellerName+"%")
|
||||||
|
}
|
||||||
|
if buyerName != "" {
|
||||||
|
query = query.Joins("LEFT JOIN users AS buyers ON sales_orders.buyer_id = buyers.id").
|
||||||
|
Where("buyers.real_name LIKE ? OR buyers.phone LIKE ?", "%"+buyerName+"%", "%"+buyerName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&orders).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询卖单列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": orders,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPurchaseOrder 获取单个买单详情
|
||||||
|
func (h *AdminOrderHandler) GetPurchaseOrder(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
query := common.GetDB().Preload("Buyer").Preload("Seller").Preload("Address")
|
||||||
|
|
||||||
|
err = query.First(&order, uint(orderID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单详情失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是代理商,检查权限
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 检查买家是否为代理商邀请的用户
|
||||||
|
if order.Buyer.ReferrerIdentityCode != user.IdentityCode {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(order))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSalesOrder 获取单个卖单详情
|
||||||
|
func (h *AdminOrderHandler) GetSalesOrder(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var order models.SalesOrder
|
||||||
|
query := common.GetDB().Preload("Seller").Preload("Buyer").Preload("SecondaryProduct").Preload("Address")
|
||||||
|
|
||||||
|
err = query.First(&order, uint(orderID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单详情失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是代理商,检查权限
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 检查买家或卖家是否为代理商邀请的用户
|
||||||
|
if order.Buyer.ReferrerIdentityCode != user.IdentityCode && order.Seller.ReferrerIdentityCode != user.IdentityCode {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(order))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePurchaseOrderStatus 更新买单状态(仅管理员)
|
||||||
|
func (h *AdminOrderHandler) UpdatePurchaseOrderStatus(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新订单状态
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData struct {
|
||||||
|
OrderStatus uint8 `json:"order_status" binding:"required"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 检查订单是否存在
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
if err := tx.First(&order, uint(orderID)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
updateFields := map[string]interface{}{
|
||||||
|
"order_status": updateData.OrderStatus,
|
||||||
|
"remark": updateData.Remark,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果状态为已完成,设置确认时间并将商品入库
|
||||||
|
if updateData.OrderStatus == 2 {
|
||||||
|
now := time.Now()
|
||||||
|
updateFields["confirm_time"] = &now
|
||||||
|
|
||||||
|
// 商品入库到买家仓库
|
||||||
|
if err := h.addToWarehouse(tx, order.BuyerID, &order); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "商品入库失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&order).Updates(updateFields).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新订单状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"message": "订单状态更新成功",
|
||||||
|
}
|
||||||
|
if updateData.OrderStatus == 2 {
|
||||||
|
result["message"] = "订单确认成功,商品已入库"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSalesOrderStatus 更新卖单状态(仅管理员)
|
||||||
|
func (h *AdminOrderHandler) UpdateSalesOrderStatus(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新订单状态
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData struct {
|
||||||
|
OrderStatus uint8 `json:"order_status" binding:"required"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单是否存在
|
||||||
|
var order models.SalesOrder
|
||||||
|
if err := common.GetDB().First(&order, uint(orderID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
updateFields := map[string]interface{}{
|
||||||
|
"order_status": updateData.OrderStatus,
|
||||||
|
"remark": updateData.Remark,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果状态为确认完成,设置完成时间
|
||||||
|
if updateData.OrderStatus == 2 {
|
||||||
|
updateFields["complete_time"] = time.Now()
|
||||||
|
db := common.GetDB()
|
||||||
|
var purcharseOrder models.PurchaseOrder
|
||||||
|
db.Where("id=?", order.PurchaseOrderID).First(&purcharseOrder)
|
||||||
|
err := h.addToWarehouse(db, purcharseOrder.BuyerID, &purcharseOrder)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "确认失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if updateData.OrderStatus == 3 {
|
||||||
|
canel(order.PurchaseOrderID, order.BuyerID)
|
||||||
|
}
|
||||||
|
if err := common.GetDB().Model(&order).Updates(updateFields).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新订单状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrderStats 获取订单统计信息
|
||||||
|
func (h *AdminOrderHandler) GetOrderStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 买单统计
|
||||||
|
purchaseQuery := db.Model(&models.PurchaseOrder{}).Where("order_status = ?", 2)
|
||||||
|
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 代理商只统计自己邀请的用户的订单
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
purchaseQuery = purchaseQuery.Where("buyer_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalPurchaseOrders int64
|
||||||
|
var totalPurchaseAmount float64
|
||||||
|
purchaseQuery.Count(&totalPurchaseOrders)
|
||||||
|
purchaseQuery.Select("COALESCE(SUM(total_amount), 0)").Scan(&totalPurchaseAmount)
|
||||||
|
|
||||||
|
stats["total_purchase_orders"] = totalPurchaseOrders
|
||||||
|
stats["total_purchase_amount"] = totalPurchaseAmount
|
||||||
|
|
||||||
|
// 卖单统计
|
||||||
|
salesQuery := db.Model(&models.SalesOrder{})
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 代理商只统计自己邀请的用户相关的订单
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
salesQuery = salesQuery.Where("buyer_id IN (?) OR seller_id IN (?)", subQuery, subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalSalesOrders int64
|
||||||
|
var totalSalesAmount float64
|
||||||
|
salesQuery.Count(&totalSalesOrders)
|
||||||
|
salesQuery.Select("COALESCE(SUM(total_amount), 0)").Scan(&totalSalesAmount)
|
||||||
|
|
||||||
|
stats["total_sales_orders"] = totalSalesOrders
|
||||||
|
stats["total_sales_amount"] = totalSalesAmount
|
||||||
|
|
||||||
|
// 今日订单统计
|
||||||
|
todayPurchaseQuery := purchaseQuery
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
todayPurchaseQuery = todayPurchaseQuery.Where("buyer_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
var todayPurchaseOrders int64
|
||||||
|
todayPurchaseQuery.Where("DATE(created_at) = CURDATE()").Count(&todayPurchaseOrders)
|
||||||
|
|
||||||
|
todaySalesQuery := salesQuery
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
todaySalesQuery = todaySalesQuery.Where("buyer_id IN (?) OR seller_id IN (?)", subQuery, subQuery)
|
||||||
|
}
|
||||||
|
var todaySalesOrders int64
|
||||||
|
todaySalesQuery.Where("DATE(created_at) = CURDATE()").Count(&todaySalesOrders)
|
||||||
|
|
||||||
|
stats["today_purchase_orders"] = todayPurchaseOrders
|
||||||
|
stats["today_sales_orders"] = todaySalesOrders
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
func canel(OrderID, UserID uint) {
|
||||||
|
db := common.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 查询订单
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
if err := tx.Where("id = ? AND buyer_id = ?", OrderID, UserID).First(&order).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态(只有待付款和待确认的订单可以取消)
|
||||||
|
if order.OrderStatus != 0 && order.OrderStatus != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态为已取消
|
||||||
|
if err := tx.Model(&order).Update("order_status", 3).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复商品库存
|
||||||
|
if order.ProductType == 1 { // 一级商品
|
||||||
|
if err := tx.Model(&models.PrimaryProduct{}).Where("id = ?", order.ProductID).
|
||||||
|
Update("stock_quantity", gorm.Expr("stock_quantity + ?", order.Quantity)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else { // 二级商品
|
||||||
|
if err := tx.Model(&models.SecondaryProduct{}).Where("id = ?", order.ProductID).
|
||||||
|
Update("quantity", gorm.Expr("quantity + ?", order.Quantity)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToWarehouse 添加商品到买家仓库
|
||||||
|
func (h *AdminOrderHandler) addToWarehouse(tx *gorm.DB, buyerID uint, order *models.PurchaseOrder) error {
|
||||||
|
|
||||||
|
var config models.SystemConfig
|
||||||
|
// 计算入库价格(根据系统配置增值)
|
||||||
|
|
||||||
|
// 检查该订单是否已经入库(防止重复入库)
|
||||||
|
var existingByOrder models.UserWarehouse
|
||||||
|
err := tx.Where("user_id = ? AND source_order_id = ? and product_type=? and product_id=?",
|
||||||
|
buyerID, order.ID, order.ProductType, order.ProductID).First(&existingByOrder).Error
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// 该订单已经入库,直接返回
|
||||||
|
return nil
|
||||||
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
|
// 查询出错
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查仓库中是否已有相同商品(不同订单的相同商品)
|
||||||
|
var existingWarehouse models.UserWarehouse
|
||||||
|
//err = tx.Where("user_id = ? AND product_id = ? AND product_type = ?",
|
||||||
|
// buyerID, order.ProductID, order.ProductType).First(&existingWarehouse).Error
|
||||||
|
|
||||||
|
_ = tx.Where("config_key= ?", "warehouse_price_rate").First(&config).Error
|
||||||
|
value, _ := strconv.ParseFloat(config.ConfigValue, 64)
|
||||||
|
warehousePrice := order.UnitPrice + value
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// 创建新的仓库记录
|
||||||
|
warehouse := models.UserWarehouse{
|
||||||
|
UserID: buyerID,
|
||||||
|
ProductID: order.ProductID,
|
||||||
|
ProductType: order.ProductType,
|
||||||
|
ProductName: order.ProductName,
|
||||||
|
ProductImages: order.ProductImages,
|
||||||
|
PurchasePrice: order.UnitPrice,
|
||||||
|
WarehousePrice: warehousePrice,
|
||||||
|
Quantity: order.Quantity,
|
||||||
|
SourceOrderID: &order.ID,
|
||||||
|
Status: 1, // 库存中
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Create(&warehouse).Error
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
// 更新现有仓库记录的数量(累加不同订单的相同商品)
|
||||||
|
return tx.Model(&existingWarehouse).Update("quantity", existingWarehouse.Quantity+order.Quantity).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminOrderHandler) GetOrderTrend(c *gin.Context) {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
type DailyStats struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]DailyStats, 0, 7)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for i := 6; i >= 0; i-- {
|
||||||
|
// 当天 00:00:00
|
||||||
|
startOfDay := time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location())
|
||||||
|
// 次日 00:00:00
|
||||||
|
endOfDay := startOfDay.AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
// 格式化显示用的 "01-02"
|
||||||
|
displayDateStr := startOfDay.Format("01-02")
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&models.PurchaseOrder{}).
|
||||||
|
Where("created_at >= ? AND created_at < ?", startOfDay, endOfDay).
|
||||||
|
Count(&count)
|
||||||
|
|
||||||
|
result = append(result, DailyStats{
|
||||||
|
Date: displayDateStr,
|
||||||
|
Count: int(count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(map[string]interface{}{
|
||||||
|
"trend": result,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecentActivities 获取最近活动
|
||||||
|
func (h *AdminOrderHandler) GetRecentActivities(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 准备返回数据结构
|
||||||
|
type Activity struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var activities []Activity
|
||||||
|
|
||||||
|
// 获取最近的订单活动(最多10条)
|
||||||
|
var recentOrders []models.PurchaseOrder
|
||||||
|
orderQuery := db.Model(&models.PurchaseOrder{}).Preload("Buyer")
|
||||||
|
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 代理商只能看到自己邀请的用户的活动
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
orderQuery = orderQuery.Where("buyer_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := orderQuery.Order("created_at DESC").Limit(10).Find(&recentOrders).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取最近活动失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将订单转换为活动
|
||||||
|
for _, order := range recentOrders {
|
||||||
|
var description string
|
||||||
|
switch order.OrderStatus {
|
||||||
|
case 0:
|
||||||
|
description = fmt.Sprintf("用户 %s 创建了新订单 %s", order.Buyer.Phone, order.OrderNo)
|
||||||
|
case 1:
|
||||||
|
description = fmt.Sprintf("用户 %s 提交了订单 %s 的付款凭证", order.Buyer.Phone, order.OrderNo)
|
||||||
|
case 2:
|
||||||
|
description = fmt.Sprintf("订单 %s 已完成", order.OrderNo)
|
||||||
|
case 3:
|
||||||
|
description = fmt.Sprintf("订单 %s 已取消", order.OrderNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
activities = append(activities, Activity{
|
||||||
|
ID: order.ID,
|
||||||
|
Description: description,
|
||||||
|
Time: order.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(map[string]interface{}{
|
||||||
|
"activities": activities,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminOrderMessageHandler 管理端订单留言处理器
|
||||||
|
type AdminOrderMessageHandler struct{}
|
||||||
|
|
||||||
|
// GetOrderMessages 获取订单留言列表
|
||||||
|
func (h *AdminOrderMessageHandler) GetOrderMessages(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("order_id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderType := c.Query("type") // purchase 或 sales
|
||||||
|
if orderType == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单类型无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 验证管理员权限
|
||||||
|
if user.SystemRole != 0 && user.SystemRole != 1 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var messages []models.OrderMessage
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 如果是买单,需要同时查询买单和对应卖单的留言
|
||||||
|
var query *gorm.DB
|
||||||
|
if orderType == "purchase" {
|
||||||
|
// 查找是否有对应的卖单
|
||||||
|
var salesOrder models.SalesOrder
|
||||||
|
err := db.Where("purchase_order_id = ?", orderID).First(&salesOrder).Error
|
||||||
|
if err == nil {
|
||||||
|
// 有对应卖单,查询两个订单的留言
|
||||||
|
query = db.Model(&models.OrderMessage{}).
|
||||||
|
Where("(order_id = ? AND order_type = ?) OR (order_id = ? AND order_type = ?)",
|
||||||
|
orderID, "purchase", salesOrder.ID, "sales")
|
||||||
|
} else {
|
||||||
|
// 没有对应卖单,只查询买单留言
|
||||||
|
query = db.Model(&models.OrderMessage{}).
|
||||||
|
Where("order_id = ? AND order_type = ?", orderID, "purchase")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 卖单,查询卖单和对应买单的留言
|
||||||
|
var salesOrder models.SalesOrder
|
||||||
|
err := db.Where("id = ?", orderID).First(&salesOrder).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query = db.Model(&models.OrderMessage{}).
|
||||||
|
Where("(order_id = ? AND order_type = ?) OR (order_id = ? AND order_type = ?)",
|
||||||
|
salesOrder.ID, "sales", salesOrder.PurchaseOrderID, "purchase")
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("User", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Select("id, customer_name, real_name, phone, system_role")
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err = query.Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&messages).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询留言列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": messages,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrderMessage 创建订单留言
|
||||||
|
func (h *AdminOrderMessageHandler) CreateOrderMessage(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("order_id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderType := c.Query("type") // purchase 或 sales
|
||||||
|
if orderType == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单类型无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 验证管理员权限
|
||||||
|
if user.SystemRole != 0 && user.SystemRole != 1 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Images string `json:"images"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证留言内容
|
||||||
|
if len(req.Content) > 500 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "留言内容不能超过500字"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证订单是否存在
|
||||||
|
db := common.GetDB()
|
||||||
|
if orderType == "purchase" {
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
if err := db.First(&order, uint(orderID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var order models.SalesOrder
|
||||||
|
if err := db.First(&order, uint(orderID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建留言记录
|
||||||
|
message := models.OrderMessage{
|
||||||
|
OrderID: uint(orderID),
|
||||||
|
OrderType: orderType,
|
||||||
|
UserID: user.ID,
|
||||||
|
Content: req.Content,
|
||||||
|
Images: req.Images,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&message).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建留言失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预加载用户信息
|
||||||
|
db.Preload("User", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Select("id, customer_name, real_name, phone, system_role")
|
||||||
|
}).First(&message, message.ID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(message))
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminPaymentHandler 管理员付款方式处理器
|
||||||
|
type AdminPaymentHandler struct{}
|
||||||
|
|
||||||
|
// GetPaymentInfo 获取管理员付款方式信息
|
||||||
|
func (h *AdminPaymentHandler) GetPaymentInfo(c *gin.Context) {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 查询系统角色为管理员的用户的付款信息
|
||||||
|
var admin models.User
|
||||||
|
err := db.Where("system_role = ?", 0).Select("main_payment_qr_image, sub_payment_qr_image").First(&admin).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询管理员付款信息失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回管理员付款方式信息
|
||||||
|
paymentInfo := map[string]interface{}{
|
||||||
|
"main_payment_qr_image": admin.MainPaymentQrImage,
|
||||||
|
"sub_payment_qr_image": admin.SubPaymentQrImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(paymentInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePaymentInfo 更新管理员付款方式信息
|
||||||
|
func (h *AdminPaymentHandler) UpdatePaymentInfo(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 验证是否为管理员
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求数据
|
||||||
|
var req struct {
|
||||||
|
MainPaymentQrImage string `json:"main_payment_qr_image"`
|
||||||
|
SubPaymentQrImage string `json:"sub_payment_qr_image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新管理员付款方式信息
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
if req.MainPaymentQrImage != "" {
|
||||||
|
updates["main_payment_qr_image"] = req.MainPaymentQrImage
|
||||||
|
}
|
||||||
|
if req.SubPaymentQrImage != "" {
|
||||||
|
updates["sub_payment_qr_image"] = req.SubPaymentQrImage
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) == 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "没有要更新的信息"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.GetDB().Model(&user).Updates(updates).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新付款方式失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的信息
|
||||||
|
var updatedUser models.User
|
||||||
|
common.GetDB().First(&updatedUser, user.ID)
|
||||||
|
|
||||||
|
paymentInfo := map[string]interface{}{
|
||||||
|
"main_payment_qr_image": updatedUser.MainPaymentQrImage,
|
||||||
|
"sub_payment_qr_image": updatedUser.SubPaymentQrImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(paymentInfo))
|
||||||
|
}
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminProductHandler 管理端商品处理器
|
||||||
|
type AdminProductHandler struct{}
|
||||||
|
|
||||||
|
// CreateProductCategoryRequest 创建商品分类请求结构体
|
||||||
|
type CreateProductCategoryRequest struct {
|
||||||
|
CategoryName string `json:"category_name" binding:"required"`
|
||||||
|
CategoryIcon string `json:"category_icon"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProductCategoryRequest 更新商品分类请求结构体
|
||||||
|
type UpdateProductCategoryRequest struct {
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
CategoryIcon string `json:"category_icon"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProductCategories 获取商品分类列表
|
||||||
|
func (h *AdminProductHandler) GetProductCategories(c *gin.Context) {
|
||||||
|
var categories []models.ProductCategory
|
||||||
|
|
||||||
|
err := common.GetDB().Order("sort_order ASC, created_at DESC").Find(&categories).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品分类失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProductCategory 创建商品分类(仅管理员)
|
||||||
|
func (h *AdminProductHandler) CreateProductCategory(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以创建分类
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateProductCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建分类对象
|
||||||
|
newCategory := models.ProductCategory{
|
||||||
|
CategoryName: req.CategoryName,
|
||||||
|
CategoryIcon: req.CategoryIcon,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
Status: req.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存分类
|
||||||
|
if err := common.GetDB().Create(&newCategory).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建商品分类失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(newCategory))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProductCategory 更新商品分类(仅管理员)
|
||||||
|
func (h *AdminProductHandler) UpdateProductCategory(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新分类
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "分类ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateProductCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查分类是否存在
|
||||||
|
var existCategory models.ProductCategory
|
||||||
|
if err := common.GetDB().First(&existCategory, uint(categoryID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品分类不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备更新数据
|
||||||
|
updateData := models.ProductCategory{
|
||||||
|
CategoryName: req.CategoryName,
|
||||||
|
CategoryIcon: req.CategoryIcon,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
Status: req.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分类信息
|
||||||
|
updateData.ID = uint(categoryID)
|
||||||
|
if err := common.GetDB().Model(&existCategory).Updates(&updateData).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新商品分类失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的分类信息
|
||||||
|
var updatedCategory models.ProductCategory
|
||||||
|
common.GetDB().First(&updatedCategory, uint(categoryID))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedCategory))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProductCategory 删除商品分类(仅管理员)
|
||||||
|
func (h *AdminProductHandler) DeleteProductCategory(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以删除分类
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "分类ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查分类下是否有商品
|
||||||
|
var productCount int64
|
||||||
|
common.GetDB().Model(&models.PrimaryProduct{}).Where("category_id = ?", categoryID).Count(&productCount)
|
||||||
|
if productCount > 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "该分类下还有商品,无法删除"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除分类
|
||||||
|
if err := common.GetDB().Delete(&models.ProductCategory{}, uint(categoryID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除商品分类失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrimaryProducts 获取一级商品列表
|
||||||
|
func (h *AdminProductHandler) GetPrimaryProducts(c *gin.Context) {
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
productName := c.Query("product_name")
|
||||||
|
status := c.Query("status")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var products []models.PrimaryProduct
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.PrimaryProduct{}).Preload("Category")
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if categoryID != "" {
|
||||||
|
query = query.Where("category_id = ?", categoryID)
|
||||||
|
}
|
||||||
|
if productName != "" {
|
||||||
|
query = query.Where("product_name LIKE ?", "%"+productName+"%")
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("sort_order ASC, created_at DESC").Find(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": products,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrimaryProduct 获取单个一级商品详情
|
||||||
|
func (h *AdminProductHandler) GetPrimaryProduct(c *gin.Context) {
|
||||||
|
productID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var product models.PrimaryProduct
|
||||||
|
err = common.GetDB().Preload("Category").First(&product, uint(productID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品详情失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(product))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePrimaryProductRequest 创建一级商品请求结构体
|
||||||
|
type CreatePrimaryProductRequest struct {
|
||||||
|
CategoryID uint `json:"category_id" binding:"required"`
|
||||||
|
ProductName string `json:"product_name" binding:"required"`
|
||||||
|
ProductDescription string `json:"product_description"`
|
||||||
|
ProductImages string `json:"product_images"`
|
||||||
|
OriginalPrice float64 `json:"original_price" binding:"required"`
|
||||||
|
CurrentPrice float64 `json:"current_price" binding:"required"`
|
||||||
|
StockQuantity int `json:"stock_quantity"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
IsHot int8 `json:"is_hot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePrimaryProduct 创建一级商品(仅管理员)
|
||||||
|
func (h *AdminProductHandler) CreatePrimaryProduct(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以创建商品
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreatePrimaryProductRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查分类是否存在
|
||||||
|
var category models.ProductCategory
|
||||||
|
if err := common.GetDB().First(&category, req.CategoryID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品分类不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建商品对象
|
||||||
|
newProduct := models.PrimaryProduct{
|
||||||
|
CategoryID: req.CategoryID,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
ProductDescription: req.ProductDescription,
|
||||||
|
ProductImages: req.ProductImages,
|
||||||
|
OriginalPrice: req.OriginalPrice,
|
||||||
|
CurrentPrice: req.CurrentPrice,
|
||||||
|
StockQuantity: req.StockQuantity,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
Status: req.Status,
|
||||||
|
IsHot: req.IsHot,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存商品
|
||||||
|
if err := common.GetDB().Create(&newProduct).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建商品失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回创建的商品(包含分类信息)
|
||||||
|
var createdProduct models.PrimaryProduct
|
||||||
|
common.GetDB().Preload("Category").First(&createdProduct, newProduct.ID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(createdProduct))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePrimaryProductRequest 更新一级商品请求结构体
|
||||||
|
type UpdatePrimaryProductRequest struct {
|
||||||
|
CategoryID uint `json:"category_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductDescription string `json:"product_description"`
|
||||||
|
ProductImages string `json:"product_images"`
|
||||||
|
OriginalPrice float64 `json:"original_price"`
|
||||||
|
CurrentPrice float64 `json:"current_price"`
|
||||||
|
StockQuantity int `json:"stock_quantity"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
IsHot int8 `json:"is_hot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePrimaryProduct 更新一级商品(仅管理员)
|
||||||
|
func (h *AdminProductHandler) UpdatePrimaryProduct(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新商品
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
productID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdatePrimaryProductRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否存在
|
||||||
|
var existProduct models.PrimaryProduct
|
||||||
|
if err := common.GetDB().First(&existProduct, uint(productID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果更新了分类,检查分类是否存在
|
||||||
|
if req.CategoryID != 0 {
|
||||||
|
var category models.ProductCategory
|
||||||
|
if err := common.GetDB().First(&category, req.CategoryID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品分类不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备更新数据
|
||||||
|
updateData := models.PrimaryProduct{
|
||||||
|
CategoryID: req.CategoryID,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
ProductDescription: req.ProductDescription,
|
||||||
|
ProductImages: req.ProductImages,
|
||||||
|
OriginalPrice: req.OriginalPrice,
|
||||||
|
CurrentPrice: req.CurrentPrice,
|
||||||
|
StockQuantity: req.StockQuantity,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
Status: req.Status,
|
||||||
|
IsHot: req.IsHot,
|
||||||
|
}
|
||||||
|
fmt.Println(updateData.IsHot)
|
||||||
|
// 更新商品信息
|
||||||
|
updateData.ID = uint(productID)
|
||||||
|
if err := common.GetDB().Model(&existProduct).Updates(&updateData).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新商品失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.GetDB().Model(&existProduct).Select("status", "is_hot").
|
||||||
|
Updates(models.PrimaryProduct{IsHot: req.IsHot, Status: req.Status})
|
||||||
|
// 返回更新后的商品信息
|
||||||
|
var updatedProduct models.PrimaryProduct
|
||||||
|
common.GetDB().Preload("Category").First(&updatedProduct, uint(productID))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedProduct))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePrimaryProduct 删除一级商品(仅管理员)
|
||||||
|
func (h *AdminProductHandler) DeletePrimaryProduct(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以删除商品
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
productID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否存在
|
||||||
|
var existProduct models.PrimaryProduct
|
||||||
|
if err := common.GetDB().First(&existProduct, uint(productID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除商品
|
||||||
|
if err := common.GetDB().Delete(&existProduct).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除商品失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecondaryProducts 获取二级商品列表
|
||||||
|
func (h *AdminProductHandler) GetSecondaryProducts(c *gin.Context) {
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
sellerID := c.Query("seller_id")
|
||||||
|
productName := c.Query("product_name")
|
||||||
|
status := c.Query("status")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var products []models.SecondaryProduct
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.SecondaryProduct{}).Preload("Seller")
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if sellerID != "" {
|
||||||
|
query = query.Where("seller_id = ?", sellerID)
|
||||||
|
}
|
||||||
|
if productName != "" {
|
||||||
|
query = query.Where("product_name LIKE ?", "%"+productName+"%")
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询二级商品列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": products,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProductStats 获取商品统计信息
|
||||||
|
func (h *AdminProductHandler) GetProductStats(c *gin.Context) {
|
||||||
|
db := common.GetDB()
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 一级商品统计
|
||||||
|
var totalPrimaryProducts int64
|
||||||
|
var onSalePrimaryProducts int64
|
||||||
|
db.Model(&models.PrimaryProduct{}).Count(&totalPrimaryProducts)
|
||||||
|
db.Model(&models.PrimaryProduct{}).Where("status = 1").Count(&onSalePrimaryProducts)
|
||||||
|
|
||||||
|
stats["total_primary_products"] = totalPrimaryProducts
|
||||||
|
stats["on_sale_primary_products"] = onSalePrimaryProducts
|
||||||
|
|
||||||
|
// 二级商品统计
|
||||||
|
var totalSecondaryProducts int64
|
||||||
|
var onSaleSecondaryProducts int64
|
||||||
|
db.Model(&models.SecondaryProduct{}).Count(&totalSecondaryProducts)
|
||||||
|
db.Model(&models.SecondaryProduct{}).Where("status = 1").Count(&onSaleSecondaryProducts)
|
||||||
|
|
||||||
|
stats["total_secondary_products"] = totalSecondaryProducts
|
||||||
|
stats["on_sale_secondary_products"] = onSaleSecondaryProducts
|
||||||
|
|
||||||
|
// 商品分类数量
|
||||||
|
var totalCategories int64
|
||||||
|
db.Model(&models.ProductCategory{}).Count(&totalCategories)
|
||||||
|
stats["total_categories"] = totalCategories
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(stats))
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScoreRecordCreateRequest 用于接收创建积分记录的请求
|
||||||
|
type ScoreRecordCreateRequest struct {
|
||||||
|
UserID uint `json:"user_id" binding:"required"`
|
||||||
|
ChangeNumber int `json:"change_number" binding:"required"`
|
||||||
|
Note string `json:"note" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminScoreHandler 管理端积分处理器
|
||||||
|
type AdminScoreHandler struct{}
|
||||||
|
|
||||||
|
// GetScoreRecords 获取积分记录列表
|
||||||
|
func (h *AdminScoreHandler) GetScoreRecords(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
userID := c.Query("user_id")
|
||||||
|
phone := c.Query("phone")
|
||||||
|
changeType := c.Query("change_type") // positive: 正数, negative: 负数
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var records []models.ScoreRecord
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.ScoreRecord{}).Preload("User")
|
||||||
|
|
||||||
|
// 如果是代理商,只能查看自己邀请的用户的积分记录
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 子查询:获取代理商邀请的用户ID列表
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
query = query.Where("user_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if userID != "" {
|
||||||
|
query = query.Where("user_id = ?", userID)
|
||||||
|
}
|
||||||
|
if phone != "" {
|
||||||
|
// 通过关联用户表搜索手机号
|
||||||
|
query = query.Joins("JOIN users ON users.id = score_records.user_id").Where("users.phone LIKE ?", "%"+phone+"%")
|
||||||
|
}
|
||||||
|
if changeType == "positive" {
|
||||||
|
query = query.Where("change_number > 0")
|
||||||
|
} else if changeType == "negative" {
|
||||||
|
query = query.Where("change_number < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询积分记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": records,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScoreRecord 获取单个积分记录详情
|
||||||
|
func (h *AdminScoreHandler) GetScoreRecord(c *gin.Context) {
|
||||||
|
recordID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "记录ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var record models.ScoreRecord
|
||||||
|
query := common.GetDB().Preload("User")
|
||||||
|
|
||||||
|
err = query.First(&record, uint(recordID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "积分记录不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询积分记录失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是代理商,检查权限
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 检查用户是否为代理商邀请的用户
|
||||||
|
if record.User.ReferrerIdentityCode != user.IdentityCode {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateScoreRecord 创建积分记录(仅管理员)
|
||||||
|
func (h *AdminScoreHandler) CreateScoreRecord(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以创建积分记录
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newRecord1 ScoreRecordCreateRequest
|
||||||
|
if err := c.ShouldBindJSON(&newRecord1); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否存在
|
||||||
|
var targetUser models.User
|
||||||
|
if err := common.GetDB().First(&targetUser, newRecord1.UserID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRecord := models.ScoreRecord{
|
||||||
|
UserID: newRecord1.UserID,
|
||||||
|
ChangeNumber: newRecord1.ChangeNumber,
|
||||||
|
Note: newRecord1.Note,
|
||||||
|
}
|
||||||
|
// 开启事务
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
|
||||||
|
// 创建积分记录
|
||||||
|
if err := tx.Create(&newRecord).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建积分记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户积分
|
||||||
|
newPoints := targetUser.CurrentPoints + newRecord.ChangeNumber
|
||||||
|
if newPoints < 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "用户积分不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&targetUser).Update("current_points", newPoints).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新用户积分失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
// 返回创建的记录(包含用户信息)
|
||||||
|
var createdRecord models.ScoreRecord
|
||||||
|
common.GetDB().Preload("User").First(&createdRecord, newRecord.ID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(createdRecord))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateScoreRecord 更新积分记录(仅管理员)
|
||||||
|
func (h *AdminScoreHandler) UpdateScoreRecord(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新积分记录
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recordID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "记录ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData struct {
|
||||||
|
ChangeNumber int `json:"change_number" binding:"required"`
|
||||||
|
Note string `json:"note" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查积分记录是否存在
|
||||||
|
var existRecord models.ScoreRecord
|
||||||
|
if err := common.GetDB().Preload("User").First(&existRecord, uint(recordID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "积分记录不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
|
||||||
|
// 计算积分差值
|
||||||
|
pointsDiff := updateData.ChangeNumber - existRecord.ChangeNumber
|
||||||
|
newUserPoints := existRecord.User.CurrentPoints + pointsDiff
|
||||||
|
|
||||||
|
if newUserPoints < 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "用户积分不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新积分记录
|
||||||
|
if err := tx.Model(&existRecord).Updates(map[string]interface{}{
|
||||||
|
"change_number": updateData.ChangeNumber,
|
||||||
|
"note": updateData.Note,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新积分记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户积分
|
||||||
|
if err := tx.Model(&existRecord.User).Update("current_points", newUserPoints).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新用户积分失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
// 返回更新后的记录
|
||||||
|
var updatedRecord models.ScoreRecord
|
||||||
|
common.GetDB().Preload("User").First(&updatedRecord, uint(recordID))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedRecord))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteScoreRecord 删除积分记录(仅管理员)
|
||||||
|
func (h *AdminScoreHandler) DeleteScoreRecord(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以删除积分记录
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recordID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "记录ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查积分记录是否存在
|
||||||
|
var existRecord models.ScoreRecord
|
||||||
|
if err := common.GetDB().Preload("User").First(&existRecord, uint(recordID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "积分记录不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
|
||||||
|
// 恢复用户积分(减去这条记录的变化值)
|
||||||
|
newUserPoints := existRecord.User.CurrentPoints - existRecord.ChangeNumber
|
||||||
|
if newUserPoints < 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "删除此记录会导致用户积分为负数"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户积分
|
||||||
|
if err := tx.Model(&existRecord.User).Update("current_points", newUserPoints).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新用户积分失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除积分记录
|
||||||
|
if err := tx.Delete(&existRecord).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除积分记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScoreStats 获取积分统计信息
|
||||||
|
func (h *AdminScoreHandler) GetScoreStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 构建基础查询
|
||||||
|
baseQuery := db.Model(&models.ScoreRecord{})
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 代理商只统计自己邀请的用户的积分记录
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
baseQuery = baseQuery.Where("user_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总积分变化记录数
|
||||||
|
var totalRecords int64
|
||||||
|
baseQuery.Count(&totalRecords)
|
||||||
|
stats["total_records"] = totalRecords
|
||||||
|
|
||||||
|
// 总积分增加
|
||||||
|
var totalIncrease int64
|
||||||
|
baseQuery.Where("change_number > 0").Select("COALESCE(SUM(change_number), 0)").Scan(&totalIncrease)
|
||||||
|
stats["total_increase"] = totalIncrease
|
||||||
|
|
||||||
|
// 总积分减少
|
||||||
|
var totalDecrease int64
|
||||||
|
baseQuery.Where("change_number < 0").Select("COALESCE(SUM(ABS(change_number)), 0)").Scan(&totalDecrease)
|
||||||
|
stats["total_decrease"] = totalDecrease
|
||||||
|
|
||||||
|
// 今日积分变化
|
||||||
|
todayQuery := baseQuery
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
subQuery := db.Model(&models.User{}).Select("id").Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
todayQuery = todayQuery.Where("user_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
var todayIncrease int64
|
||||||
|
todayQuery.Where("change_number > 0 AND DATE(created_at) = CURDATE()").Select("COALESCE(SUM(change_number), 0)").Scan(&todayIncrease)
|
||||||
|
stats["today_increase"] = todayIncrease
|
||||||
|
|
||||||
|
var todayDecrease int64
|
||||||
|
todayQuery.Where("change_number < 0 AND DATE(created_at) = CURDATE()").Select("COALESCE(SUM(ABS(change_number)), 0)").Scan(&todayDecrease)
|
||||||
|
stats["today_decrease"] = todayDecrease
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(stats))
|
||||||
|
}
|
||||||
@@ -0,0 +1,633 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/middleware"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminUserHandler 管理端用户处理器
|
||||||
|
type AdminUserHandler struct{}
|
||||||
|
|
||||||
|
// GetUsers 获取用户列表
|
||||||
|
func (h *AdminUserHandler) GetUsers(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
phone := c.Query("phone")
|
||||||
|
realName := c.Query("real_name")
|
||||||
|
systemRole := c.Query("system_role")
|
||||||
|
identityCode := c.Query("identity_code")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var users []models.User
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.User{})
|
||||||
|
|
||||||
|
// 如果是代理商,只能查看自己邀请的用户
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
query = query.Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if phone != "" {
|
||||||
|
query = query.Where("phone LIKE ?", "%"+phone+"%")
|
||||||
|
}
|
||||||
|
if realName != "" {
|
||||||
|
query = query.Where("real_name LIKE ?", "%"+realName+"%")
|
||||||
|
}
|
||||||
|
if systemRole != "" {
|
||||||
|
query = query.Where("system_role = ?", systemRole)
|
||||||
|
} else {
|
||||||
|
query = query.Where("system_role > 0")
|
||||||
|
}
|
||||||
|
if identityCode != "" {
|
||||||
|
query = query.Where("identity_code LIKE ? OR referrer_identity_code LIKE ?", "%"+identityCode+"%", "%"+identityCode+"%")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&users).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询用户列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": users,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser 获取单个用户信息
|
||||||
|
func (h *AdminUserHandler) GetUser(c *gin.Context) {
|
||||||
|
userID, err := middleware.GetUserIDFromParam(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err = common.GetDB().First(&user, userID).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询用户信息失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser 创建用户(仅管理员)
|
||||||
|
func (h *AdminUserHandler) CreateUser(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以创建用户
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newUser models.User
|
||||||
|
if err := c.ShouldBindJSON(&newUser); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
var existUser models.User
|
||||||
|
if err := common.GetDB().Where("phone = ?", newUser.Phone).First(&existUser).Error; err == nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "手机号已存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if newUser.SystemRole == 1 {
|
||||||
|
newUser.IdentityCode = "T" + uuid.New().String()[:8]
|
||||||
|
} else {
|
||||||
|
newUser.IdentityCode = "U" + uuid.New().String()[:8]
|
||||||
|
}
|
||||||
|
if newUser.Password != "" {
|
||||||
|
newUser.Password = fmt.Sprintf("%x", md5.Sum([]byte(newUser.Password)))
|
||||||
|
}
|
||||||
|
// 保存用户
|
||||||
|
if err := common.GetDB().Create(&newUser).Error; err != nil {
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建用户失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(newUser))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser 更新用户信息(仅管理员)
|
||||||
|
func (h *AdminUserHandler) UpdateUser(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以更新用户
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "用户ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData models.User
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(updateData)
|
||||||
|
// 检查用户是否存在
|
||||||
|
var existUser models.User
|
||||||
|
if err := common.GetDB().First(&existUser, uint(userID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
updateData.ID = uint(userID)
|
||||||
|
if updateData.Password != "" {
|
||||||
|
updateData.Password = fmt.Sprintf("%x", md5.Sum([]byte(updateData.Password)))
|
||||||
|
}
|
||||||
|
if err := common.GetDB().Model(&existUser).Updates(&updateData).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新用户信息失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := common.GetDB().Model(&existUser).Updates(map[string]interface{}{
|
||||||
|
"status": updateData.Status,
|
||||||
|
}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新用户信息失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 返回更新后的用户信息
|
||||||
|
var updatedUser models.User
|
||||||
|
common.GetDB().First(&updatedUser, uint(userID))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedUser))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser 删除用户(仅管理员)
|
||||||
|
func (h *AdminUserHandler) DeleteUser(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以删除用户
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "用户ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否存在
|
||||||
|
var existUser models.User
|
||||||
|
if err := common.GetDB().First(&existUser, uint(userID)).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
if err := common.GetDB().Delete(&existUser).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除用户失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserStats 获取用户统计信息
|
||||||
|
func (h *AdminUserHandler) GetUserStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 如果是代理商,只统计自己邀请的用户
|
||||||
|
baseQuery := db.Model(&models.User{})
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
baseQuery = baseQuery.Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总用户数
|
||||||
|
var totalUsers int64
|
||||||
|
baseQuery.Count(&totalUsers)
|
||||||
|
stats["total_users"] = totalUsers
|
||||||
|
|
||||||
|
// 按角色统计
|
||||||
|
var roleCounts []struct {
|
||||||
|
SystemRole uint8 `json:"system_role"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
roleQuery := baseQuery
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
roleQuery = roleQuery.Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
roleQuery.Select("system_role, count(*) as count").Group("system_role").Scan(&roleCounts)
|
||||||
|
stats["role_counts"] = roleCounts
|
||||||
|
|
||||||
|
// 今日新增用户
|
||||||
|
var todayUsers int64
|
||||||
|
todayQuery := baseQuery
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
todayQuery = todayQuery.Where("referrer_identity_code = ?", user.IdentityCode)
|
||||||
|
}
|
||||||
|
todayQuery.Where("DATE(created_at) = CURDATE()").Count(&todayUsers)
|
||||||
|
stats["today_users"] = todayUsers
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserExpiryDate 设置用户过期时间(仅管理员)
|
||||||
|
func (h *AdminUserHandler) SetUserExpiryDate(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 只有管理员可以设置用户过期时间
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "用户ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ExpiryDate *string `json:"expiry_date"` // ISO 8601 格式,如 "2024-12-31T23:59:59Z",null表示永不过期
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 检查用户是否存在
|
||||||
|
var targetUser models.User
|
||||||
|
if err := db.First(&targetUser, uint(userID)).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询用户失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止设置管理员的过期时间
|
||||||
|
if targetUser.SystemRole == 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "不能设置管理员的过期时间"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析过期时间
|
||||||
|
var expiryDate *time.Time
|
||||||
|
if req.ExpiryDate != nil && *req.ExpiryDate != "" {
|
||||||
|
parsedTime, err := time.Parse(time.RFC3339, *req.ExpiryDate)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "过期时间格式错误,请使用ISO 8601格式"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiryDate = &parsedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户过期时间
|
||||||
|
if err := db.Model(&targetUser).Update("expiry_date", expiryDate).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "设置用户过期时间失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var message string
|
||||||
|
if expiryDate == nil {
|
||||||
|
message = "用户已设置为永不过期"
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("用户过期时间已设置为:%s", expiryDate.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
"expiry_date": expiryDate,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchUserStats 搜索用户统计信息
|
||||||
|
func (h *AdminUserHandler) SearchUserStats(c *gin.Context) {
|
||||||
|
// 获取当前用户信息
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
currentUseruser := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 仅管理员可访问
|
||||||
|
if currentUseruser.SystemRole > 1 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := c.Query("query")
|
||||||
|
if query == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "搜索条件不能为空"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取时间范围参数
|
||||||
|
startDate := c.Query("start_date")
|
||||||
|
endDate := c.Query("end_date")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var user models.User
|
||||||
|
//err2 := db.Where("id=?", currentUseruser.ID).First(¤tUseruser).Error
|
||||||
|
//if err2 != nil {
|
||||||
|
// c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
// 按手机号或姓名搜索用户
|
||||||
|
err := db.Where("phone = ? OR customer_name LIKE ? OR real_name LIKE ?",
|
||||||
|
query, "%"+query+"%", "%"+query+"%").First(&user).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "未找到匹配的用户"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询用户失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentUseruser.SystemRole == 1 && user.ReferrerIdentityCode != currentUseruser.IdentityCode {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 统计该用户的数据
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 构建采购订单查询
|
||||||
|
purchaseQuery := db.Model(&models.PurchaseOrder{}).Where("buyer_id = ? and order_status=?", user.ID, 2)
|
||||||
|
if startDate != "" && endDate != "" {
|
||||||
|
purchaseQuery = purchaseQuery.Where("created_at >= ? AND created_at <= ?", startDate+" 00:00:00", endDate+" 23:59:59")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 采购订单统计
|
||||||
|
var purchaseOrdersCount int64
|
||||||
|
var totalPurchaseAmount float64
|
||||||
|
purchaseQuery.Count(&purchaseOrdersCount)
|
||||||
|
purchaseQuery.Select("COALESCE(SUM(total_amount), 0)").Scan(&totalPurchaseAmount)
|
||||||
|
|
||||||
|
// 构建销售订单查询
|
||||||
|
salesQuery := db.Model(&models.SalesOrder{}).Where("seller_id = ? and order_status=?", user.ID, 2)
|
||||||
|
if startDate != "" && endDate != "" {
|
||||||
|
salesQuery = salesQuery.Where("created_at >= ? AND created_at <= ?", startDate+" 00:00:00", endDate+" 23:59:59")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销售订单统计
|
||||||
|
var salesOrdersCount int64
|
||||||
|
var totalSalesAmount float64
|
||||||
|
salesQuery.Count(&salesOrdersCount)
|
||||||
|
salesQuery.Select("COALESCE(SUM(total_amount), 0)").Scan(&totalSalesAmount)
|
||||||
|
|
||||||
|
// 库存统计
|
||||||
|
var warehouseItemsCount int64
|
||||||
|
var totalWarehouseQuantity int64
|
||||||
|
if startDate != "" && endDate != "" {
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? and created_at >= ? AND created_at <= ?", user.ID, startDate+" 00:00:00", endDate+" 23:59:59").Count(&warehouseItemsCount)
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? and created_at >= ? AND created_at <= ?", user.ID, startDate+" 00:00:00", endDate+" 23:59:59").Select("COALESCE(SUM(quantity), 0)").Scan(&totalWarehouseQuantity)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? ", user.ID).Count(&warehouseItemsCount)
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? ", user.ID).Select("COALESCE(SUM(quantity), 0)").Scan(&totalWarehouseQuantity)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 积分余额
|
||||||
|
var scoreBalance int
|
||||||
|
//db.Model(&models.ScoreRecord{}).Where("user_id = ?", user.ID).Select("COALESCE(SUM(score_change), 0)").Scan(&scoreBalance)
|
||||||
|
scoreBalance = user.CurrentPoints
|
||||||
|
// 最后活动时间(最近的订单时间)
|
||||||
|
var lastActivityTime *time.Time
|
||||||
|
var lastPurchaseTime, lastSalesTime time.Time
|
||||||
|
|
||||||
|
// 查找最近的采购订单时间
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where("user_id = ?", user.ID).
|
||||||
|
Select("created_at").Order("created_at DESC").Limit(1).Scan(&lastPurchaseTime)
|
||||||
|
|
||||||
|
// 查找最近的销售订单时间
|
||||||
|
db.Model(&models.SalesOrder{}).Where("user_id = ?", user.ID).
|
||||||
|
Select("created_at").Order("created_at DESC").Limit(1).Scan(&lastSalesTime)
|
||||||
|
|
||||||
|
// 取两个时间中的最大值
|
||||||
|
if !lastPurchaseTime.IsZero() && !lastSalesTime.IsZero() {
|
||||||
|
if lastPurchaseTime.After(lastSalesTime) {
|
||||||
|
lastActivityTime = &lastPurchaseTime
|
||||||
|
} else {
|
||||||
|
lastActivityTime = &lastSalesTime
|
||||||
|
}
|
||||||
|
} else if !lastPurchaseTime.IsZero() {
|
||||||
|
lastActivityTime = &lastPurchaseTime
|
||||||
|
} else if !lastSalesTime.IsZero() {
|
||||||
|
lastActivityTime = &lastSalesTime
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["purchase_orders_count"] = purchaseOrdersCount
|
||||||
|
stats["total_purchase_amount"] = totalPurchaseAmount
|
||||||
|
stats["sales_orders_count"] = salesOrdersCount
|
||||||
|
stats["total_sales_amount"] = totalSalesAmount
|
||||||
|
stats["warehouse_items_count"] = warehouseItemsCount
|
||||||
|
stats["total_warehouse_quantity"] = totalWarehouseQuantity
|
||||||
|
stats["score_balance"] = scoreBalance
|
||||||
|
stats["last_activity_time"] = lastActivityTime
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"user": user,
|
||||||
|
"stats": stats,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllUserWarehouses 获取所有用户库存汇总(管理员专用)
|
||||||
|
func (h *AdminUserHandler) GetAllUserWarehouses(c *gin.Context) {
|
||||||
|
// 获取当前用户信息
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
currentUseruser := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 仅管理员可访问
|
||||||
|
if currentUseruser.SystemRole > 1 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
searchQuery := c.Query("search") // 搜索手机号或姓名
|
||||||
|
productType := c.Query("product_type") // 商品类型:1一级商品,2二级商品
|
||||||
|
status := c.Query("status") // 状态:0已出售,1库存中
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 构建库存记录查询(带用户信息)
|
||||||
|
warehouseQuery := db.Model(&models.UserWarehouse{}).
|
||||||
|
Select("user_warehouse.*, users.phone, users.customer_name, users.real_name").
|
||||||
|
Joins("JOIN users ON users.id = user_warehouse.user_id")
|
||||||
|
|
||||||
|
// 搜索用户(基于手机号或姓名)
|
||||||
|
if searchQuery != "" {
|
||||||
|
warehouseQuery = warehouseQuery.Where("users.phone LIKE ? OR users.customer_name LIKE ? OR users.real_name LIKE ?",
|
||||||
|
"%"+searchQuery+"%", "%"+searchQuery+"%", "%"+searchQuery+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用商品类型筛选
|
||||||
|
if productType != "" {
|
||||||
|
warehouseQuery = warehouseQuery.Where("user_warehouse.product_type = ?", productType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用状态筛选
|
||||||
|
if status != "" {
|
||||||
|
warehouseQuery = warehouseQuery.Where("user_warehouse.status = ?", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
var total int64
|
||||||
|
warehouseQuery.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询库存记录
|
||||||
|
var userWarehouses []models.UserWarehouse
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := warehouseQuery.Offset(offset).Limit(pageSize).Order("user_warehouse.created_at DESC").Find(&userWarehouses).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询用户库存列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换数据结构以包含用户信息,并按用户ID、商品类型、商品名称、状态聚合
|
||||||
|
var resultList []struct {
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
RealName string `json:"real_name"`
|
||||||
|
ProductType uint8 `json:"product_type"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
TotalQuantity int `json:"total_quantity"`
|
||||||
|
TotalValue float64 `json:"total_value"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按用户ID、商品类型、商品名称、状态聚合库存
|
||||||
|
userWarehouseMap := make(map[string]*struct {
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
RealName string `json:"real_name"`
|
||||||
|
ProductType uint8 `json:"product_type"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
TotalQuantity int `json:"total_quantity"`
|
||||||
|
TotalValue float64 `json:"total_value"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, warehouse := range userWarehouses {
|
||||||
|
// 生成唯一键:用户ID_商品类型_商品名称_状态
|
||||||
|
key := fmt.Sprintf("%d_%d_%s_%d", warehouse.UserID, warehouse.ProductType, warehouse.ProductName, warehouse.Status)
|
||||||
|
|
||||||
|
// 获取用户信息(第一次出现时)
|
||||||
|
var user models.User
|
||||||
|
db.First(&user, warehouse.UserID)
|
||||||
|
|
||||||
|
if item, exists := userWarehouseMap[key]; exists {
|
||||||
|
// 累加数量和价值
|
||||||
|
item.TotalQuantity += warehouse.Quantity
|
||||||
|
item.TotalValue += float64(warehouse.Quantity) * warehouse.WarehousePrice
|
||||||
|
} else {
|
||||||
|
// 创建新的聚合记录
|
||||||
|
userWarehouseMap[key] = &struct {
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
RealName string `json:"real_name"`
|
||||||
|
ProductType uint8 `json:"product_type"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
TotalQuantity int `json:"total_quantity"`
|
||||||
|
TotalValue float64 `json:"total_value"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
}{
|
||||||
|
UserID: warehouse.UserID,
|
||||||
|
Phone: user.Phone,
|
||||||
|
CustomerName: user.CustomerName,
|
||||||
|
RealName: user.RealName,
|
||||||
|
ProductType: warehouse.ProductType,
|
||||||
|
ProductName: warehouse.ProductName,
|
||||||
|
TotalQuantity: warehouse.Quantity,
|
||||||
|
TotalValue: float64(warehouse.Quantity) * warehouse.WarehousePrice,
|
||||||
|
Status: warehouse.Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为切片
|
||||||
|
for _, item := range userWarehouseMap {
|
||||||
|
resultList = append(resultList, *item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": resultList,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"awesomeProject/internal/services"
|
||||||
|
jwtutil "awesomeProject/pkg/utils"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthHandler 认证处理器
|
||||||
|
type AuthHandler struct{}
|
||||||
|
|
||||||
|
// LoginRequest 登录请求结构
|
||||||
|
type LoginRequest struct {
|
||||||
|
Phone string `json:"phone" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse 登录响应结构
|
||||||
|
type LoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
User models.User `json:"user"`
|
||||||
|
}
|
||||||
|
type TokenGen struct {
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
SystemRole int8 `json:"system_role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户
|
||||||
|
var user models.User
|
||||||
|
err := common.GetDB().Where("phone = ?", req.Phone).First(&user).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "手机号或密码错误"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "登录失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码(这里简单使用MD5,实际应该使用bcrypt)
|
||||||
|
hashedPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.Password)))
|
||||||
|
fmt.Println(hashedPassword)
|
||||||
|
if user.Password != hashedPassword {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "手机号或密码错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户状态
|
||||||
|
if user.Status != 1 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户已被禁用,请联系管理员"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成JWT token
|
||||||
|
claims := TokenGen{
|
||||||
|
UserID: user.ID,
|
||||||
|
Phone: user.Phone,
|
||||||
|
SystemRole: int8(user.SystemRole),
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwtutil.GenerateToken(claims)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "生成token失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回登录结果
|
||||||
|
response := LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRequest 注册请求结构体
|
||||||
|
type RegisterRequest struct {
|
||||||
|
models.User
|
||||||
|
SMSCode string `json:"sms_code"` // 短信验证码
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 用户注册
|
||||||
|
func (h *AuthHandler) Register(c *gin.Context) {
|
||||||
|
var req RegisterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证短信验证码(必填)
|
||||||
|
if req.SMSCode == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请输入短信验证码"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证短信验证码
|
||||||
|
smsService := services.GetSMSService()
|
||||||
|
if !smsService.VerifyCode(req.Phone, req.SMSCode) {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "短信验证码错误或已过期"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
var existUser models.User
|
||||||
|
if err := common.GetDB().Where("phone = ?", req.Phone).First(&existUser).Error; err == nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "手机号已被注册"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := req.User
|
||||||
|
|
||||||
|
// 生成身份码(简单实现,实际应该使用更复杂的算法)
|
||||||
|
user.IdentityCode = fmt.Sprintf("U%v", uuid.New().String()[:8])
|
||||||
|
|
||||||
|
// 密码加密(这里简单使用MD5,实际应该使用bcrypt)
|
||||||
|
hashedPassword := fmt.Sprintf("%x", md5.Sum([]byte(user.Password)))
|
||||||
|
user.Password = hashedPassword
|
||||||
|
|
||||||
|
// 默认为普通用户
|
||||||
|
if user.SystemRole == 0 {
|
||||||
|
user.SystemRole = 2
|
||||||
|
|
||||||
|
}
|
||||||
|
user.Status = 0
|
||||||
|
// 保存用户
|
||||||
|
if err := common.GetDB().Create(&user).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "注册失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.GetDB().Model(&user).Update("status", 0)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentUser 获取当前用户信息
|
||||||
|
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||||
|
// 从token中获取用户信息
|
||||||
|
tokenMap, exists := c.Get("tokenMap")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "未授权访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := tokenMap.(map[string]interface{})
|
||||||
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "用户信息无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := uint(userIDFloat)
|
||||||
|
|
||||||
|
// 查询用户信息
|
||||||
|
var user models.User
|
||||||
|
if err := common.GetDB().First(&user, userID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProfileRequest 更新个人信息请求结构体
|
||||||
|
type UpdateProfileRequest struct {
|
||||||
|
CustomerName string `json:"customer_name" binding:"required"`
|
||||||
|
RealName string `json:"real_name" binding:"required"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
CompanyName string `json:"company_name"`
|
||||||
|
PersonalIntro string `json:"personal_intro"`
|
||||||
|
BusinessLicense string `json:"business_license"`
|
||||||
|
IDCardFront string `json:"id_card_front"`
|
||||||
|
IDCardBack string `json:"id_card_back"`
|
||||||
|
MainPaymentCode string `json:"main_payment_code"`
|
||||||
|
BackupPaymentCode string `json:"backup_payment_code"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProfile 更新个人信息
|
||||||
|
func (h *AuthHandler) UpdateProfile(c *gin.Context) {
|
||||||
|
// 从token中获取用户信息
|
||||||
|
tokenMap, exists := c.Get("tokenMap")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "未授权访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := tokenMap.(map[string]interface{})
|
||||||
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "用户信息无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := uint(userIDFloat)
|
||||||
|
|
||||||
|
var req UpdateProfileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询当前用户
|
||||||
|
var user models.User
|
||||||
|
if err := common.GetDB().First(&user, userID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
updateData := models.User{
|
||||||
|
CustomerName: req.CustomerName,
|
||||||
|
RealName: req.RealName,
|
||||||
|
Avatar: req.Avatar,
|
||||||
|
CompanyName: req.CompanyName,
|
||||||
|
PersonalIntro: req.PersonalIntro,
|
||||||
|
BusinessLicenseImage: req.BusinessLicense,
|
||||||
|
IdCardFrontImage: req.IDCardFront,
|
||||||
|
IdCardBackImage: req.IDCardBack,
|
||||||
|
MainPaymentQrImage: &req.MainPaymentCode,
|
||||||
|
SubPaymentQrImage: &req.BackupPaymentCode,
|
||||||
|
Phone: req.Phone,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData.ID = userID
|
||||||
|
if err := common.GetDB().Model(&user).Updates(&updateData).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新个人信息失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的用户信息
|
||||||
|
var updatedUser models.User
|
||||||
|
common.GetDB().First(&updatedUser, userID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedUser))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePasswordRequest 修改密码请求结构体
|
||||||
|
type ChangePasswordRequest struct {
|
||||||
|
CurrentPassword string `json:"current_password" binding:"required"`
|
||||||
|
NewPassword string `json:"new_password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword 修改密码
|
||||||
|
func (h *AuthHandler) ChangePassword(c *gin.Context) {
|
||||||
|
// 从token中获取用户信息
|
||||||
|
tokenMap, exists := c.Get("tokenMap")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "未授权访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := tokenMap.(map[string]interface{})
|
||||||
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusOK, common.Error(401, "用户信息无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := uint(userIDFloat)
|
||||||
|
|
||||||
|
var req ChangePasswordRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询当前用户
|
||||||
|
var user models.User
|
||||||
|
if err := common.GetDB().First(&user, userID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证当前密码
|
||||||
|
hashedCurrentPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.CurrentPassword)))
|
||||||
|
if hashedCurrentPassword != user.Password {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "当前密码错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密新密码
|
||||||
|
hashedNewPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.NewPassword)))
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
if err := common.GetDB().Model(&user).Update("password", hashedNewPassword).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "修改密码失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSMSRequest 发送短信验证码请求结构体
|
||||||
|
type SendSMSRequest struct {
|
||||||
|
Phone string `json:"phone" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSMS 发送短信验证码
|
||||||
|
func (h *AuthHandler) SendSMS(c *gin.Context) {
|
||||||
|
var req SendSMSRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "参数错误: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取客户端IP地址
|
||||||
|
clientIP := getClientIP(c)
|
||||||
|
|
||||||
|
// 调用短信服务发送验证码
|
||||||
|
smsService := services.GetSMSService()
|
||||||
|
code, err := smsService.SendSMS(req.Phone, clientIP)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据环境决定是否返回验证码(开发环境返回,生产环境不返回)
|
||||||
|
config := common.MineConfig
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"message": "验证码已发送",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅在开发环境返回验证码,方便测试
|
||||||
|
if config != nil && config.App.Env == "development" {
|
||||||
|
response["code"] = code
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClientIP 获取客户端真实IP地址
|
||||||
|
func getClientIP(c *gin.Context) string {
|
||||||
|
// 优先从 X-Forwarded-For 获取(经过代理时)
|
||||||
|
ip := c.GetHeader("X-Forwarded-For")
|
||||||
|
if ip != "" {
|
||||||
|
// X-Forwarded-For 可能包含多个IP,取第一个
|
||||||
|
ips := strings.Split(ip, ",")
|
||||||
|
if len(ips) > 0 {
|
||||||
|
return strings.TrimSpace(ips[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 X-Real-IP 获取
|
||||||
|
ip = c.GetHeader("X-Real-IP")
|
||||||
|
if ip != "" {
|
||||||
|
return strings.TrimSpace(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后从 RemoteAddr 获取
|
||||||
|
ip = c.ClientIP()
|
||||||
|
return ip
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitHandler 初始化处理器
|
||||||
|
type InitHandler struct{}
|
||||||
|
|
||||||
|
// InitDefaultAdmin 初始化默认管理员
|
||||||
|
func (h *InitHandler) InitDefaultAdmin() error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 检查是否已有管理员用户
|
||||||
|
var adminCount int64
|
||||||
|
db.Model(&models.User{}).Where("system_role = 0").Count(&adminCount)
|
||||||
|
if adminCount > 0 {
|
||||||
|
return nil // 已有管理员,不重复创建
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认管理员
|
||||||
|
hashedPassword := fmt.Sprintf("%x", md5.Sum([]byte("admin123")))
|
||||||
|
admin := models.User{
|
||||||
|
Phone: "13800138000",
|
||||||
|
SystemRole: 0, // 管理员
|
||||||
|
CustomerName: "系统管理员",
|
||||||
|
RealName: "Administrator",
|
||||||
|
Password: hashedPassword,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&admin).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建默认管理员失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新身份码
|
||||||
|
admin.IdentityCode = fmt.Sprintf("A%08d", admin.ID)
|
||||||
|
if err := db.Model(&admin).Update("identity_code", admin.IdentityCode).Error; err != nil {
|
||||||
|
return fmt.Errorf("更新管理员身份码失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("默认管理员创建成功: 手机号: %s, 密码: admin123", admin.Phone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitDefaultAgent 初始化默认代理商(可选)
|
||||||
|
func (h *InitHandler) InitDefaultAgent() error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 检查是否已有代理商用户
|
||||||
|
var agent models.User
|
||||||
|
err := db.Where("phone = ?", "13800138001").First(&agent).Error
|
||||||
|
if err == nil {
|
||||||
|
return nil // 代理商已存在
|
||||||
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
|
return fmt.Errorf("查询代理商失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认代理商
|
||||||
|
hashedPassword := fmt.Sprintf("%x", md5.Sum([]byte("agent123")))
|
||||||
|
agentUser := models.User{
|
||||||
|
Phone: "13800138001",
|
||||||
|
SystemRole: 1, // 代理商
|
||||||
|
CustomerName: "测试代理商",
|
||||||
|
RealName: "Test Agent",
|
||||||
|
Password: hashedPassword,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&agentUser).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建默认代理商失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新身份码
|
||||||
|
agentUser.IdentityCode = fmt.Sprintf("T%08d", agentUser.ID)
|
||||||
|
if err := db.Model(&agentUser).Update("identity_code", agentUser.IdentityCode).Error; err != nil {
|
||||||
|
return fmt.Errorf("更新代理商身份码失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("默认代理商创建成功: 手机号: %s, 密码: agent123", agentUser.Phone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitTestUsers 初始化测试用户
|
||||||
|
func (h *InitHandler) InitTestUsers() error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 获取代理商身份码
|
||||||
|
var agent models.User
|
||||||
|
if err := db.Where("system_role = 1").First(&agent).Error; err != nil {
|
||||||
|
return nil // 没有代理商,跳过创建测试用户
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建测试用户
|
||||||
|
testUsers := []models.User{
|
||||||
|
{
|
||||||
|
Phone: "13800138002",
|
||||||
|
SystemRole: 2, // 普通用户
|
||||||
|
CustomerName: "测试用户1",
|
||||||
|
RealName: "Test User 1",
|
||||||
|
Password: fmt.Sprintf("%x", md5.Sum([]byte("user123"))),
|
||||||
|
ReferrerIdentityCode: agent.IdentityCode,
|
||||||
|
Status: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Phone: "13800138003",
|
||||||
|
SystemRole: 2, // 普通用户
|
||||||
|
CustomerName: "测试用户2",
|
||||||
|
RealName: "Test User 2",
|
||||||
|
Password: fmt.Sprintf("%x", md5.Sum([]byte("user123"))),
|
||||||
|
ReferrerIdentityCode: agent.IdentityCode,
|
||||||
|
Status: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, user := range testUsers {
|
||||||
|
// 检查用户是否已存在
|
||||||
|
var existUser models.User
|
||||||
|
if err := db.Where("phone = ?", user.Phone).First(&existUser).Error; err == nil {
|
||||||
|
continue // 用户已存在,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&user).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建测试用户失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新身份码
|
||||||
|
user.IdentityCode = fmt.Sprintf("U%08d", user.ID)
|
||||||
|
if err := db.Model(&user).Update("identity_code", user.IdentityCode).Error; err != nil {
|
||||||
|
return fmt.Errorf("更新用户身份码失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testUsers[i] = user
|
||||||
|
log.Printf("测试用户创建成功: 手机号: %s, 密码: user123", user.Phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitAll 初始化所有数据
|
||||||
|
func (h *InitHandler) InitAll() error {
|
||||||
|
// 初始化默认管理员
|
||||||
|
if err := h.InitDefaultAdmin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化默认代理商
|
||||||
|
if err := h.InitDefaultAgent(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化测试用户
|
||||||
|
if err := h.InitTestUsers(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UploadHandler 文件上传处理器
|
||||||
|
type UploadHandler struct{}
|
||||||
|
|
||||||
|
// NewUploadHandler 创建文件上传处理器实例
|
||||||
|
func NewUploadHandler() *UploadHandler {
|
||||||
|
return &UploadHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadResponse 上传响应
|
||||||
|
type UploadResponse struct {
|
||||||
|
FileName string `json:"fileName"` // 原文件名
|
||||||
|
NewFileName string `json:"newFileName"` // 新文件名
|
||||||
|
URL string `json:"url"` // 访问URL
|
||||||
|
Size int64 `json:"size"` // 文件大小(字节)
|
||||||
|
Type string `json:"type"` // 文件类型
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFile 上传文件
|
||||||
|
func (h *UploadHandler) UploadFile(c *gin.Context) {
|
||||||
|
// 获取上传的文件
|
||||||
|
file, header, err := c.Request.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取上传文件失败: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 检查文件大小限制 (10MB)
|
||||||
|
maxSize := int64(10 * 1024 * 1024) // 10MB
|
||||||
|
if header.Size > maxSize {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "文件大小超过限制(最大10MB)"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件类型
|
||||||
|
if !isAllowedFileType(header.Filename) {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "不支持的文件类型"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建保存目录
|
||||||
|
saveDir := "./public/file"
|
||||||
|
if err := os.MkdirAll(saveDir, os.ModePerm); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建保存目录失败: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成新的文件名
|
||||||
|
newFileName, err := generateFileName(header.Filename, file)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "生成文件名失败: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完整的保存路径
|
||||||
|
savePath := filepath.Join(saveDir, newFileName)
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
if err := saveUploadedFile(file, savePath); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "保存文件失败: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建响应
|
||||||
|
response := UploadResponse{
|
||||||
|
FileName: header.Filename,
|
||||||
|
NewFileName: newFileName,
|
||||||
|
URL: "/back/file/" + newFileName,
|
||||||
|
Size: header.Size,
|
||||||
|
Type: getFileType(header.Filename),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateFileName 生成唯一的文件名
|
||||||
|
func generateFileName(originalName string, file io.Reader) (string, error) {
|
||||||
|
// 获取文件扩展名
|
||||||
|
ext := filepath.Ext(originalName)
|
||||||
|
|
||||||
|
// 重置文件指针到开头
|
||||||
|
if seeker, ok := file.(io.Seeker); ok {
|
||||||
|
seeker.Seek(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容用于生成哈希
|
||||||
|
hasher := md5.New()
|
||||||
|
if _, err := io.Copy(hasher, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置文件指针到开头
|
||||||
|
if seeker, ok := file.(io.Seeker); ok {
|
||||||
|
seeker.Seek(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件名:时间戳 + MD5哈希前8位 + 扩展名
|
||||||
|
timestamp := time.Now().Format("20060102150405")
|
||||||
|
hash := fmt.Sprintf("%x", hasher.Sum(nil))[:8]
|
||||||
|
newName := fmt.Sprintf("%s_%s%s", timestamp, hash, ext)
|
||||||
|
|
||||||
|
return newName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveUploadedFile 保存上传的文件
|
||||||
|
func saveUploadedFile(src io.Reader, dst string) error {
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, src)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAllowedFileType 检查是否为允许的文件类型
|
||||||
|
func isAllowedFileType(filename string) bool {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
|
||||||
|
// 允许的文件类型
|
||||||
|
allowedTypes := map[string]bool{
|
||||||
|
// 图片类型
|
||||||
|
".jpg": true,
|
||||||
|
".jpeg": true,
|
||||||
|
".png": true,
|
||||||
|
".gif": true,
|
||||||
|
".bmp": true,
|
||||||
|
".webp": true,
|
||||||
|
".svg": true,
|
||||||
|
// 文档类型
|
||||||
|
".pdf": true,
|
||||||
|
".doc": true,
|
||||||
|
".docx": true,
|
||||||
|
".xls": true,
|
||||||
|
".xlsx": true,
|
||||||
|
".ppt": true,
|
||||||
|
".pptx": true,
|
||||||
|
".txt": true,
|
||||||
|
".rtf": true,
|
||||||
|
// 压缩包类型
|
||||||
|
".zip": true,
|
||||||
|
".rar": true,
|
||||||
|
".7z": true,
|
||||||
|
".tar": true,
|
||||||
|
".gz": true,
|
||||||
|
// 视频类型
|
||||||
|
".mp4": true,
|
||||||
|
".avi": true,
|
||||||
|
".mov": true,
|
||||||
|
".wmv": true,
|
||||||
|
".flv": true,
|
||||||
|
".webm": true,
|
||||||
|
// 音频类型
|
||||||
|
".mp3": true,
|
||||||
|
".wav": true,
|
||||||
|
".flac": true,
|
||||||
|
".aac": true,
|
||||||
|
".ogg": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedTypes[ext]
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileType 获取文件类型
|
||||||
|
func getFileType(filename string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
|
||||||
|
imageTypes := map[string]bool{
|
||||||
|
".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
|
||||||
|
".bmp": true, ".webp": true, ".svg": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
documentTypes := map[string]bool{
|
||||||
|
".pdf": true, ".doc": true, ".docx": true, ".xls": true,
|
||||||
|
".xlsx": true, ".ppt": true, ".pptx": true, ".txt": true, ".rtf": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
videoTypes := map[string]bool{
|
||||||
|
".mp4": true, ".avi": true, ".mov": true, ".wmv": true,
|
||||||
|
".flv": true, ".webm": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTypes := map[string]bool{
|
||||||
|
".mp3": true, ".wav": true, ".flac": true, ".aac": true, ".ogg": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveTypes := map[string]bool{
|
||||||
|
".zip": true, ".rar": true, ".7z": true, ".tar": true, ".gz": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageTypes[ext] {
|
||||||
|
return "image"
|
||||||
|
} else if documentTypes[ext] {
|
||||||
|
return "document"
|
||||||
|
} else if videoTypes[ext] {
|
||||||
|
return "video"
|
||||||
|
} else if audioTypes[ext] {
|
||||||
|
return "audio"
|
||||||
|
} else if archiveTypes[ext] {
|
||||||
|
return "archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "other"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUploadFiles 批量上传文件
|
||||||
|
func (h *UploadHandler) BatchUploadFiles(c *gin.Context) {
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取上传文件失败: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files := form.File["files"]
|
||||||
|
if len(files) == 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "没有选择文件"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制批量上传数量
|
||||||
|
if len(files) > 10 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "一次最多上传10个文件"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses []UploadResponse
|
||||||
|
var failedFiles []string
|
||||||
|
|
||||||
|
// 创建保存目录
|
||||||
|
saveDir := "./public/file"
|
||||||
|
if err := os.MkdirAll(saveDir, os.ModePerm); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建保存目录失败: "+err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, header := range files {
|
||||||
|
// 检查文件大小
|
||||||
|
if header.Size > int64(10*1024*1024) {
|
||||||
|
failedFiles = append(failedFiles, header.Filename+" (文件过大)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件类型
|
||||||
|
if !isAllowedFileType(header.Filename) {
|
||||||
|
failedFiles = append(failedFiles, header.Filename+" (不支持的类型)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
file, err := header.Open()
|
||||||
|
if err != nil {
|
||||||
|
failedFiles = append(failedFiles, header.Filename+" (打开失败)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成新文件名
|
||||||
|
newFileName, err := generateFileName(header.Filename, file)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
failedFiles = append(failedFiles, header.Filename+" (生成文件名失败)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
savePath := filepath.Join(saveDir, newFileName)
|
||||||
|
if err := saveUploadedFile(file, savePath); err != nil {
|
||||||
|
file.Close()
|
||||||
|
failedFiles = append(failedFiles, header.Filename+" (保存失败)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
// 添加到成功列表
|
||||||
|
responses = append(responses, UploadResponse{
|
||||||
|
FileName: header.Filename,
|
||||||
|
NewFileName: newFileName,
|
||||||
|
URL: "/back/file/" + newFileName,
|
||||||
|
Size: header.Size,
|
||||||
|
Type: getFileType(header.Filename),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"success": responses,
|
||||||
|
"failed": failedFiles,
|
||||||
|
"totalCount": len(files),
|
||||||
|
"successCount": len(responses),
|
||||||
|
"failedCount": len(failedFiles),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAddressHandler 用户收货地址处理器
|
||||||
|
type UserAddressHandler struct{}
|
||||||
|
|
||||||
|
// GetAddressList 获取用户收货地址列表
|
||||||
|
func (h *UserAddressHandler) GetAddressList(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var addresses []models.UserAddress
|
||||||
|
err := common.GetDB().Where("user_id = ?", user.ID).Order("is_default DESC, created_at DESC").Find(&addresses).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取收货地址失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(addresses))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAddress 获取单个收货地址
|
||||||
|
func (h *UserAddressHandler) GetAddress(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
addressID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "地址ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var address models.UserAddress
|
||||||
|
err = common.GetDB().Where("id = ? AND user_id = ?", addressID, user.ID).First(&address).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "收货地址不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取收货地址失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(address))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAddress 创建收货地址
|
||||||
|
func (h *UserAddressHandler) CreateAddress(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var address models.UserAddress
|
||||||
|
if err := c.ShouldBindJSON(&address); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户ID
|
||||||
|
address.UserID = user.ID
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
|
||||||
|
// 如果设置为默认地址,先取消其他地址的默认状态
|
||||||
|
if address.IsDefault == 1 {
|
||||||
|
if err := tx.Model(&models.UserAddress{}).Where("user_id = ?", user.ID).Update("is_default", 0).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新默认地址状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建地址
|
||||||
|
if err := tx.Create(&address).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建收货地址失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(address))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAddress 更新收货地址
|
||||||
|
func (h *UserAddressHandler) UpdateAddress(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
addressID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "地址ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查地址是否存在且属于当前用户
|
||||||
|
var existAddress models.UserAddress
|
||||||
|
err = common.GetDB().Where("id = ? AND user_id = ?", addressID, user.ID).First(&existAddress).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "收货地址不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取收货地址失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData models.UserAddress
|
||||||
|
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
|
||||||
|
// 如果设置为默认地址,先取消其他地址的默认状态
|
||||||
|
if updateData.IsDefault == 1 {
|
||||||
|
if err := tx.Model(&models.UserAddress{}).Where("user_id = ? AND id != ?", user.ID, addressID).Update("is_default", 0).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新默认地址状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新地址信息
|
||||||
|
updateData.UserID = user.ID // 确保不会被修改
|
||||||
|
if err := tx.Model(&existAddress).Updates(updateData).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新收货地址失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
// 返回更新后的地址
|
||||||
|
var updatedAddress models.UserAddress
|
||||||
|
common.GetDB().Where("id = ?", addressID).First(&updatedAddress)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(updatedAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAddress 删除收货地址
|
||||||
|
func (h *UserAddressHandler) DeleteAddress(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
addressID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "地址ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查地址是否存在且属于当前用户
|
||||||
|
var address models.UserAddress
|
||||||
|
err = common.GetDB().Where("id = ? AND user_id = ?", addressID, user.ID).First(&address).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "收货地址不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取收货地址失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除地址
|
||||||
|
if err := common.GetDB().Delete(&address).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "删除收货地址失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultAddress 设置默认收货地址
|
||||||
|
func (h *UserAddressHandler) SetDefaultAddress(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
addressID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "地址ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查地址是否存在且属于当前用户
|
||||||
|
var address models.UserAddress
|
||||||
|
err = common.GetDB().Where("id = ? AND user_id = ?", addressID, user.ID).First(&address).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "收货地址不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "获取收货地址失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
|
||||||
|
// 取消其他地址的默认状态
|
||||||
|
if err := tx.Model(&models.UserAddress{}).Where("user_id = ?", user.ID).Update("is_default", 0).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新默认地址状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置当前地址为默认
|
||||||
|
if err := tx.Model(&address).Update("is_default", 1).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "设置默认地址失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(nil))
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserBannerHandler 用户端轮播图处理器
|
||||||
|
type UserBannerHandler struct{}
|
||||||
|
|
||||||
|
// GetActiveBanners 获取启用的轮播图列表
|
||||||
|
func (h *UserBannerHandler) GetActiveBanners(c *gin.Context) {
|
||||||
|
var banners []models.Banner
|
||||||
|
|
||||||
|
// 查询启用的轮播图,按排序顺序排列
|
||||||
|
err := common.GetDB().Where("status = ?", 1).Order("sort_order ASC, created_at DESC").Find(&banners).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询轮播图失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(banners))
|
||||||
|
}
|
||||||
@@ -0,0 +1,863 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserOrderHandler 用户订单处理器
|
||||||
|
type UserOrderHandler struct{}
|
||||||
|
|
||||||
|
// CreatePurchaseOrder 创建买单
|
||||||
|
func (h *UserOrderHandler) CreatePurchaseOrder(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ProductID uint `json:"product_id" binding:"required"`
|
||||||
|
ProductType uint8 `json:"product_type" binding:"required"` // 1一级商品,2二级商品
|
||||||
|
ProductName string `json:"product_name" binding:"required"`
|
||||||
|
ProductImages string `json:"product_images"`
|
||||||
|
UnitPrice float64 `json:"unit_price" binding:"required"`
|
||||||
|
Quantity int `json:"quantity" binding:"required"`
|
||||||
|
TotalAmount float64 `json:"total_amount" binding:"required"`
|
||||||
|
AddressID uint `json:"address_id" binding:"required"`
|
||||||
|
PaymentProofImage string `json:"payment_proof_image"` // 不再强制要求
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证商品类型
|
||||||
|
if req.ProductType != 1 && req.ProductType != 2 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品类型无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数量和金额
|
||||||
|
if req.Quantity <= 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "购买数量必须大于0"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.TotalAmount != req.UnitPrice*float64(req.Quantity) {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单金额计算错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 验证收货地址是否属于当前用户
|
||||||
|
var address models.UserAddress
|
||||||
|
if err := tx.Where("id = ? AND user_id = ?", req.AddressID, user.ID).First(&address).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "收货地址不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sellerID *uint
|
||||||
|
//var productInfo interface{}
|
||||||
|
|
||||||
|
// 根据商品类型进行不同的处理
|
||||||
|
if req.ProductType == 1 { // 一级商品
|
||||||
|
var primaryProduct models.PrimaryProduct
|
||||||
|
if err := tx.First(&primaryProduct, req.ProductID).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品状态和库存
|
||||||
|
if primaryProduct.Status != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品已下架"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if primaryProduct.StockQuantity < req.Quantity {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "库存不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少库存,增加销量
|
||||||
|
if err := tx.Model(&primaryProduct).Updates(map[string]interface{}{
|
||||||
|
"stock_quantity": primaryProduct.StockQuantity - req.Quantity,
|
||||||
|
"sales_count": primaryProduct.SalesCount + req.Quantity,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新商品库存失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//productInfo = primaryProduct
|
||||||
|
|
||||||
|
} else { // 二级商品
|
||||||
|
var secondaryProduct models.SecondaryProduct
|
||||||
|
if err := tx.Preload("Seller").First(&secondaryProduct, req.ProductID).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品状态和库存
|
||||||
|
if secondaryProduct.Status != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品已下架"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if secondaryProduct.Quantity < req.Quantity {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "库存不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是自己的商品
|
||||||
|
if secondaryProduct.SellerID == user.ID {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "不能购买自己的商品"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var config models.SystemConfig
|
||||||
|
tx.Where("config_key=?", "limit_days").First(&config)
|
||||||
|
value, _ := strconv.Atoi(config.ConfigValue)
|
||||||
|
|
||||||
|
if value > 0 {
|
||||||
|
fiveDaysAgo := time.Now().AddDate(0, 0, -value)
|
||||||
|
var existingOrderCount int64
|
||||||
|
if err := tx.Model(&models.PurchaseOrder{}).Where(
|
||||||
|
"buyer_id = ? AND seller_id = ? AND product_type = 2 AND created_at > ? AND order_status != 3",
|
||||||
|
user.ID, secondaryProduct.SellerID, fiveDaysAgo,
|
||||||
|
).Count(&existingOrderCount).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "检查购买记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingOrderCount > 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, fmt.Sprintf("%v天内只能向同一个卖家购买一次商品", value)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少库存,增加浏览量
|
||||||
|
if err := tx.Model(&secondaryProduct).Updates(map[string]interface{}{
|
||||||
|
"quantity": secondaryProduct.Quantity - req.Quantity,
|
||||||
|
"view_count": secondaryProduct.ViewCount + 1,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新商品库存失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sellerID = &secondaryProduct.SellerID
|
||||||
|
//productInfo = secondaryProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成订单编号
|
||||||
|
orderNo := fmt.Sprintf("PO%d%d", time.Now().Unix(), user.ID)
|
||||||
|
|
||||||
|
// 创建买单
|
||||||
|
orderStatus := 0 // 默认待付款
|
||||||
|
if req.PaymentProofImage != "" {
|
||||||
|
orderStatus = 1 // 如果有付款凭证,设为待确认
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseOrder := models.PurchaseOrder{
|
||||||
|
OrderNo: orderNo,
|
||||||
|
BuyerID: user.ID,
|
||||||
|
SellerID: sellerID,
|
||||||
|
ProductID: req.ProductID,
|
||||||
|
ProductType: req.ProductType,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
ProductImages: req.ProductImages,
|
||||||
|
UnitPrice: req.UnitPrice,
|
||||||
|
Quantity: req.Quantity,
|
||||||
|
TotalAmount: req.TotalAmount,
|
||||||
|
AddressID: &req.AddressID,
|
||||||
|
OrderStatus: uint8(orderStatus),
|
||||||
|
PaymentProofImage: req.PaymentProofImage,
|
||||||
|
Remark: req.Remark,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&purchaseOrder).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建订单失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//如果是二级商品,创建对应的卖单
|
||||||
|
if req.ProductType == 2 {
|
||||||
|
salesOrder := models.SalesOrder{
|
||||||
|
OrderNo: fmt.Sprintf("SO%d%d", time.Now().Unix(), *sellerID),
|
||||||
|
SellerID: *sellerID,
|
||||||
|
BuyerID: user.ID,
|
||||||
|
SecondaryProductID: req.ProductID,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
ProductImages: req.ProductImages,
|
||||||
|
SellingPrice: req.UnitPrice,
|
||||||
|
Quantity: req.Quantity,
|
||||||
|
TotalAmount: req.TotalAmount,
|
||||||
|
AddressID: &req.AddressID,
|
||||||
|
OrderStatus: 0, // 待付款
|
||||||
|
PaymentProofImage: req.PaymentProofImage,
|
||||||
|
PurchaseOrderID: purchaseOrder.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&salesOrder).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建卖单失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
// 返回订单信息
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"order_id": purchaseOrder.ID,
|
||||||
|
"order_no": purchaseOrder.OrderNo,
|
||||||
|
"status": "订单已提交,等待确认",
|
||||||
|
"message": "订单创建成功",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPurchaseOrders 获取买单列表
|
||||||
|
func (h *UserOrderHandler) GetPurchaseOrders(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
order_id, _ := strconv.Atoi(c.DefaultQuery("order_id", "0"))
|
||||||
|
// 状态过滤
|
||||||
|
status := c.Query("status")
|
||||||
|
startDate := c.Query("start_date")
|
||||||
|
endDate := c.Query("end_date")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var orders []models.PurchaseOrder
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.PurchaseOrder{}).Where("buyer_id = ?", user.ID).
|
||||||
|
Preload("Address").Preload("Seller")
|
||||||
|
|
||||||
|
// 添加状态过滤
|
||||||
|
if status != "" && status != "all" {
|
||||||
|
switch status {
|
||||||
|
case "pending":
|
||||||
|
query = query.Where("order_status = 0")
|
||||||
|
case "confirming":
|
||||||
|
query = query.Where("order_status = 1")
|
||||||
|
case "completed":
|
||||||
|
query = query.Where("order_status = 2")
|
||||||
|
case "cancelled":
|
||||||
|
query = query.Where("order_status = 3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加日期过滤
|
||||||
|
if startDate != "" {
|
||||||
|
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
||||||
|
}
|
||||||
|
if endDate != "" {
|
||||||
|
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||||
|
}
|
||||||
|
if order_id > 0 {
|
||||||
|
query = query.Where("id = ?", order_id)
|
||||||
|
}
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&orders).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
var totalAmount float64
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where("buyer_id = ? AND order_status = 2", user.ID).Select("COALESCE(SUM(total_amount), 0)").Scan(&totalAmount)
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": orders,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"total_amount": totalAmount,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSalesOrders 获取卖单列表
|
||||||
|
func (h *UserOrderHandler) GetSalesOrders(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态过滤
|
||||||
|
status := c.Query("status")
|
||||||
|
startDate := c.Query("start_date")
|
||||||
|
endDate := c.Query("end_date")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var orders []models.SalesOrder
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.SalesOrder{}).Where("seller_id = ?", user.ID).Preload("Buyer").Preload("Address")
|
||||||
|
|
||||||
|
// 添加状态过滤
|
||||||
|
if status != "" && status != "all" {
|
||||||
|
switch status {
|
||||||
|
case "pending":
|
||||||
|
query = query.Where("order_status = 0")
|
||||||
|
case "confirming":
|
||||||
|
query = query.Where("order_status = 1")
|
||||||
|
case "completed":
|
||||||
|
query = query.Where("order_status = 2")
|
||||||
|
case "cancelled":
|
||||||
|
query = query.Where("order_status = 3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加日期过滤
|
||||||
|
if startDate != "" {
|
||||||
|
query = query.Where("created_at >= ?", startDate+" 00:00:00")
|
||||||
|
}
|
||||||
|
if endDate != "" {
|
||||||
|
query = query.Where("created_at <= ?", endDate+" 23:59:59")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&orders).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询卖单列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
var totalAmount float64
|
||||||
|
db.Model(&models.SalesOrder{}).Where("seller_id = ? AND order_status = 2", user.ID).Select("COALESCE(SUM(total_amount), 0)").Scan(&totalAmount)
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": orders,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"total_amount": totalAmount,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrderStats 获取订单统计信息(待付款和待确认数量)
|
||||||
|
func (h *UserOrderHandler) GetOrderStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 买单统计
|
||||||
|
var purchasePendingCount int64 // 待付款
|
||||||
|
var purchaseConfirmingCount int64 // 待确认
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where("buyer_id = ? AND order_status = ?", user.ID, 0).Count(&purchasePendingCount)
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where("buyer_id = ? AND order_status = ?", user.ID, 1).Count(&purchaseConfirmingCount)
|
||||||
|
|
||||||
|
// 卖单统计
|
||||||
|
var salesPendingCount int64 // 待付款
|
||||||
|
var salesConfirmingCount int64 // 待确认
|
||||||
|
db.Model(&models.SalesOrder{}).Where("seller_id = ? AND order_status = ?", user.ID, 0).Count(&salesPendingCount)
|
||||||
|
db.Model(&models.SalesOrder{}).Where("seller_id = ? AND order_status = ?", user.ID, 1).Count(&salesConfirmingCount)
|
||||||
|
|
||||||
|
stats["purchase_pending_count"] = purchasePendingCount
|
||||||
|
stats["purchase_confirming_count"] = purchaseConfirmingCount
|
||||||
|
stats["sales_pending_count"] = salesPendingCount
|
||||||
|
stats["sales_confirming_count"] = salesConfirmingCount
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmPurchaseOrder 确认买单(买家确认收货)
|
||||||
|
func (h *UserOrderHandler) ConfirmPurchaseOrder(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 买家确认买单(确认收货)
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
if err := tx.Where("id = ? AND buyer_id = ?", orderID, user.ID).First(&order).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.OrderStatus != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单状态不允许确认"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态为已完成
|
||||||
|
now := time.Now()
|
||||||
|
if err := tx.Model(&order).Updates(map[string]interface{}{
|
||||||
|
"order_status": 2,
|
||||||
|
"confirm_time": &now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "确认订单失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品入库
|
||||||
|
if err := h.addToWarehouse(tx, user.ID, &order); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "商品入库失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"message": "订单确认成功,商品已入库",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmSalesOrder 确认卖单(卖家确认发货)
|
||||||
|
func (h *UserOrderHandler) ConfirmSalesOrder(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卖家确认卖单
|
||||||
|
var order models.SalesOrder
|
||||||
|
tx := common.GetDB().Begin()
|
||||||
|
if err := tx.Where("id = ? AND seller_id = ?", orderID, user.ID).First(&order).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.OrderStatus != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单状态不允许确认"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新卖单状态为已完成
|
||||||
|
now := time.Now()
|
||||||
|
if err := tx.Model(&order).Updates(models.SalesOrder{
|
||||||
|
OrderStatus: 2,
|
||||||
|
CompleteTime: &now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "确认订单失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时更新对应的买单状态
|
||||||
|
if err := tx.Model(&models.PurchaseOrder{}).Where(
|
||||||
|
"id=?",
|
||||||
|
order.PurchaseOrderID,
|
||||||
|
).Updates(models.PurchaseOrder{
|
||||||
|
OrderStatus: 2,
|
||||||
|
ConfirmTime: &now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新买单状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config models.SystemConfig
|
||||||
|
tx.Where("config_key=?", "warehouse_price_rate").First(&config)
|
||||||
|
vaule, _ := strconv.Atoi(config.ConfigValue)
|
||||||
|
newRecord := models.ScoreRecord{
|
||||||
|
UserID: user.ID,
|
||||||
|
Note: "系统策略",
|
||||||
|
ChangeNumber: -vaule,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newRecord).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建积分记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户积分
|
||||||
|
newPoints := user.CurrentPoints + newRecord.ChangeNumber
|
||||||
|
if newPoints < 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "用户积分不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&user).Update("current_points", newPoints).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新用户积分失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
var purcharseOrder models.PurchaseOrder
|
||||||
|
db := common.GetDB()
|
||||||
|
db.Where("id=?", order.PurchaseOrderID).First(&purcharseOrder)
|
||||||
|
err = h.addToWarehouse(db, order.BuyerID, &purcharseOrder)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "添加用户仓库失败"))
|
||||||
|
}
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"message": "订单确认成功",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToWarehouse 添加商品到买家仓库
|
||||||
|
func (h *UserOrderHandler) addToWarehouse(tx *gorm.DB, buyerID uint, order *models.PurchaseOrder) error {
|
||||||
|
|
||||||
|
fmt.Println("order::", order)
|
||||||
|
// 检查该订单是否已经入库(防止重复入库)
|
||||||
|
var existingByOrder models.UserWarehouse
|
||||||
|
err := tx.Where("user_id = ? AND source_order_id = ? and product_type=? and product_id=?",
|
||||||
|
buyerID, order.ID, order.ProductType, order.ProductID).First(&existingByOrder).Error
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// 该订单已经入库,直接返回
|
||||||
|
return nil
|
||||||
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
|
// 查询出错
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config models.SystemConfig
|
||||||
|
tx.Where("config_key=?", "warehouse_price_rate").First(&config)
|
||||||
|
value, _ := strconv.Atoi(config.ConfigValue)
|
||||||
|
// 计算入库价格(根据系统配置增值)
|
||||||
|
warehousePrice := order.UnitPrice + float64(value)
|
||||||
|
|
||||||
|
// 检查仓库中是否已有相同商品(不同订单的相同商品)
|
||||||
|
var existingWarehouse models.UserWarehouse
|
||||||
|
//err = tx.Where("user_id = ? AND product_id = ? AND product_type = ?",
|
||||||
|
// buyerID, order.ProductID, order.ProductType).First(&existingWarehouse).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// 创建新的仓库记录
|
||||||
|
warehouse := models.UserWarehouse{
|
||||||
|
UserID: buyerID,
|
||||||
|
ProductID: order.ProductID,
|
||||||
|
ProductType: order.ProductType,
|
||||||
|
ProductName: order.ProductName,
|
||||||
|
ProductImages: order.ProductImages,
|
||||||
|
PurchasePrice: order.UnitPrice,
|
||||||
|
WarehousePrice: warehousePrice,
|
||||||
|
Quantity: order.Quantity,
|
||||||
|
SourceOrderID: &order.ID,
|
||||||
|
Status: 1, // 库存中
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Create(&warehouse).Error
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
// 更新现有仓库记录的数量(累加不同订单的相同商品)
|
||||||
|
return tx.Model(&existingWarehouse).Update("quantity", existingWarehouse.Quantity+order.Quantity).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadPaymentProof 上传付款凭证
|
||||||
|
func (h *UserOrderHandler) UploadPaymentProof(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
PaymentProofImage string `json:"payment_proof_image" binding:"required"`
|
||||||
|
ProductType uint `json:"product_type" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 查询订单
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
if err := tx.Where("id = ? AND buyer_id = ?", orderID, user.ID).First(&order).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态
|
||||||
|
if order.OrderStatus != 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单状态不允许上传付款凭证"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态为待确认,并保存付款凭证
|
||||||
|
if err := tx.Model(&order).Updates(map[string]interface{}{
|
||||||
|
"payment_proof_image": req.PaymentProofImage,
|
||||||
|
"order_status": 1, // 待确认
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新订单失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var saleOrder models.SalesOrder
|
||||||
|
if req.ProductType == 2 {
|
||||||
|
|
||||||
|
if err := tx.Where("purchase_order_id = ?", order.ID).First(&saleOrder).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(200, common.Error(400, "查找不到有效订单"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再更新,这里可以使用 Model 指定要更新的对象
|
||||||
|
// 注意:这仍然会基于 saleOrder 的主键 ID 更新,但因为我们刚查出来,所以是安全的。
|
||||||
|
// 但更清晰的方式是直接用 Where。
|
||||||
|
|
||||||
|
// 更好的方式是:直接基于原始条件更新
|
||||||
|
if err := tx.Where("purchase_order_id = ?", order.ID).Updates(models.SalesOrder{
|
||||||
|
OrderStatus: 1,
|
||||||
|
PaymentProofImage: req.PaymentProofImage,
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(200, common.Error(400, "更新错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"message": "付款凭证上传成功,订单已提交等待确认",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelOrder 取消订单
|
||||||
|
func (h *UserOrderHandler) CancelOrder(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 查询订单
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
if err := tx.Where("id = ? AND buyer_id = ?", orderID, user.ID).First(&order).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态(只有待付款和待确认的订单可以取消)
|
||||||
|
if order.OrderStatus != 0 && order.OrderStatus != 1 {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单状态不允许取消"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态为已取消
|
||||||
|
if err := tx.Model(&order).Update("order_status", 3).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "取消订单失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复商品库存
|
||||||
|
if order.ProductType == 1 { // 一级商品
|
||||||
|
if err := tx.Model(&models.PrimaryProduct{}).Where("id = ?", order.ProductID).
|
||||||
|
Update("stock_quantity", gorm.Expr("stock_quantity + ?", order.Quantity)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "恢复库存失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else { // 二级商品
|
||||||
|
if err := tx.Model(&models.SecondaryProduct{}).Where("id = ?", order.ProductID).
|
||||||
|
Update("quantity", gorm.Expr("quantity + ?", order.Quantity)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "恢复库存失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"message": "订单已取消",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPurchaseOrderDetail 获取买单详情
|
||||||
|
func (h *UserOrderHandler) GetPurchaseOrderDetail(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var order models.PurchaseOrder
|
||||||
|
query := common.GetDB().Where("buyer_id = ?", user.ID).
|
||||||
|
Preload("Buyer").
|
||||||
|
Preload("Seller").
|
||||||
|
Preload("Address")
|
||||||
|
|
||||||
|
err = query.First(&order, uint(orderID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单详情失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adminID := uint(1)
|
||||||
|
if order.ProductType == 1 {
|
||||||
|
if order.SellerID == nil {
|
||||||
|
order.SellerID = &adminID
|
||||||
|
var seller models.User
|
||||||
|
|
||||||
|
common.GetDB().Where("id=?", order.SellerID).First(&seller)
|
||||||
|
order.Seller = &seller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(order))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSalesOrderDetail 获取卖单详情
|
||||||
|
func (h *UserOrderHandler) GetSalesOrderDetail(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
var order models.SalesOrder
|
||||||
|
query := common.GetDB().Where("seller_id = ?", user.ID).
|
||||||
|
Preload("Seller").
|
||||||
|
Preload("Buyer").
|
||||||
|
Preload("Address")
|
||||||
|
|
||||||
|
err = query.First(&order, uint(orderID)).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询订单详情失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(order))
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserOrderMessageHandler 用户端订单留言处理器
|
||||||
|
type UserOrderMessageHandler struct{}
|
||||||
|
|
||||||
|
// GetOrderMessages 获取订单留言列表
|
||||||
|
func (h *UserOrderMessageHandler) GetOrderMessages(c *gin.Context) {
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("order_id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderType := c.Query("type") // purchase 或 sales
|
||||||
|
if orderType == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单类型无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var messages []models.OrderMessage
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 如果是买单,需要同时查询买单和对应卖单的留言
|
||||||
|
var query *gorm.DB
|
||||||
|
if orderType == "purchase" {
|
||||||
|
// 查找是否有对应的卖单
|
||||||
|
var salesOrder models.SalesOrder
|
||||||
|
err := db.Where("purchase_order_id = ?", orderID).First(&salesOrder).Error
|
||||||
|
if err == nil {
|
||||||
|
// 有对应卖单,查询两个订单的留言
|
||||||
|
query = db.Model(&models.OrderMessage{}).
|
||||||
|
Where("(order_id = ? AND order_type = ?) OR (order_id = ? AND order_type = ?)",
|
||||||
|
orderID, "purchase", salesOrder.ID, "sales")
|
||||||
|
} else {
|
||||||
|
// 没有对应卖单,只查询买单留言
|
||||||
|
query = db.Model(&models.OrderMessage{}).
|
||||||
|
Where("order_id = ? AND order_type = ?", orderID, "purchase")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 卖单,查询卖单和对应买单的留言
|
||||||
|
var salesOrder models.SalesOrder
|
||||||
|
err := db.Where("id = ?", orderID).First(&salesOrder).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "订单不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query = db.Model(&models.OrderMessage{}).
|
||||||
|
Where("(order_id = ? AND order_type = ?) OR (order_id = ? AND order_type = ?)",
|
||||||
|
salesOrder.ID, "sales", salesOrder.PurchaseOrderID, "purchase")
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Preload("User", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Select("id, customer_name, real_name, phone, system_role")
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err = query.Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&messages).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询留言列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": messages,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrderMessage 创建订单留言
|
||||||
|
func (h *UserOrderMessageHandler) CreateOrderMessage(c *gin.Context) {
|
||||||
|
orderID, err := strconv.ParseUint(c.Param("order_id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "订单ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 验证用户是否有权限给此订单留言
|
||||||
|
orderType := h.getUserOrderType(uint(orderID), user.ID)
|
||||||
|
if orderType == "" {
|
||||||
|
c.JSON(http.StatusOK, common.Error(403, "权限不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Images string `json:"images"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证留言内容
|
||||||
|
if len(req.Content) > 500 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "留言内容不能超过500字"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建留言记录
|
||||||
|
message := models.OrderMessage{
|
||||||
|
OrderID: uint(orderID),
|
||||||
|
OrderType: orderType,
|
||||||
|
UserID: user.ID,
|
||||||
|
Content: req.Content,
|
||||||
|
Images: req.Images,
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
if err := db.Create(&message).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建留言失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预加载用户信息
|
||||||
|
db.Preload("User", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Select("id, customer_name, real_name, phone")
|
||||||
|
}).First(&message, message.ID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOrderPermission 检查用户是否有权限访问订单留言
|
||||||
|
func (h *UserOrderMessageHandler) checkOrderPermission(orderID uint, userID uint) bool {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 检查买单权限
|
||||||
|
var purchaseCount int64
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where("id = ? AND buyer_id = ?", orderID, userID).Count(&purchaseCount)
|
||||||
|
if purchaseCount > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查卖单权限
|
||||||
|
var salesCount int64
|
||||||
|
db.Model(&models.SalesOrder{}).Where("id = ? AND seller_id = ?", orderID, userID).Count(&salesCount)
|
||||||
|
if salesCount > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为买单的卖家(二级商品订单)
|
||||||
|
var purchaseOrder models.PurchaseOrder
|
||||||
|
if err := db.Where("id = ? AND product_type = 2", orderID).First(&purchaseOrder).Error; err == nil {
|
||||||
|
// 查找对应的二级商品是否属于当前用户
|
||||||
|
var secondaryCount int64
|
||||||
|
db.Model(&models.SecondaryProduct{}).Where("id = ? AND seller_id = ?", purchaseOrder.ProductID, userID).Count(&secondaryCount)
|
||||||
|
if secondaryCount > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserOrderType 获取用户在订单中的角色类型
|
||||||
|
func (h *UserOrderMessageHandler) getUserOrderType(orderID uint, userID uint) string {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 检查是否为买单的买家
|
||||||
|
var purchaseCount int64
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where("id = ? AND buyer_id = ?", orderID, userID).Count(&purchaseCount)
|
||||||
|
if purchaseCount > 0 {
|
||||||
|
return "purchase"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为卖单的卖家
|
||||||
|
var salesCount int64
|
||||||
|
db.Model(&models.SalesOrder{}).Where("id = ? AND (seller_id = ?)", orderID, userID).Count(&salesCount)
|
||||||
|
if salesCount > 0 || userID == 1 {
|
||||||
|
return "sales"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为买单的卖家(二级商品订单)
|
||||||
|
var purchaseOrder models.PurchaseOrder
|
||||||
|
if err := db.Where("id = ? AND product_type = 2", orderID).First(&purchaseOrder).Error; err == nil {
|
||||||
|
// 查找对应的二级商品是否属于当前用户
|
||||||
|
var secondaryCount int64
|
||||||
|
db.Model(&models.SecondaryProduct{}).Where("id = ? AND seller_id = ?", purchaseOrder.ProductID, userID).Count(&secondaryCount)
|
||||||
|
if secondaryCount > 0 {
|
||||||
|
return "purchase" // 对于买单的卖家,也归类为purchase类型
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserPaymentHandler 用户收款方式处理器
|
||||||
|
type UserPaymentHandler struct{}
|
||||||
|
|
||||||
|
// GetPaymentInfo 获取用户收款方式信息
|
||||||
|
func (h *UserPaymentHandler) GetPaymentInfo(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 返回收款方式信息
|
||||||
|
paymentInfo := map[string]interface{}{
|
||||||
|
"main_payment_qr_image": user.MainPaymentQrImage,
|
||||||
|
"sub_payment_qr_image": user.SubPaymentQrImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(paymentInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePaymentInfo 更新用户收款方式信息
|
||||||
|
func (h *UserPaymentHandler) UpdatePaymentInfo(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 解析请求数据
|
||||||
|
var req struct {
|
||||||
|
MainPaymentQrImage string `json:"main_payment_qr_image"`
|
||||||
|
SubPaymentQrImage string `json:"sub_payment_qr_image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户收款方式信息
|
||||||
|
updates := map[string]interface{}{}
|
||||||
|
if req.MainPaymentQrImage != "" {
|
||||||
|
updates["main_payment_qr_image"] = req.MainPaymentQrImage
|
||||||
|
}
|
||||||
|
if req.SubPaymentQrImage != "" {
|
||||||
|
updates["sub_payment_qr_image"] = req.SubPaymentQrImage
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) == 0 {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "没有要更新的信息"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.GetDB().Model(&user).Updates(updates).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新收款方式失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的信息
|
||||||
|
var updatedUser models.User
|
||||||
|
common.GetDB().First(&updatedUser, user.ID)
|
||||||
|
|
||||||
|
paymentInfo := map[string]interface{}{
|
||||||
|
"main_payment_qr_image": updatedUser.MainPaymentQrImage,
|
||||||
|
"sub_payment_qr_image": updatedUser.SubPaymentQrImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(paymentInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSellerPaymentInfo 获取卖家收款方式信息
|
||||||
|
func (h *UserPaymentHandler) GetSellerPaymentInfo(c *gin.Context) {
|
||||||
|
sellerID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "卖家ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var seller models.User
|
||||||
|
err = common.GetDB().Select("main_payment_qr_image, sub_payment_qr_image").First(&seller, sellerID).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "卖家不存在"))
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询卖家信息失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回卖家收款方式信息
|
||||||
|
paymentInfo := map[string]interface{}{
|
||||||
|
"main_payment_qr_image": seller.MainPaymentQrImage,
|
||||||
|
"sub_payment_qr_image": seller.SubPaymentQrImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(paymentInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserPaymentHandler) GetSellerInfo(c *gin.Context) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserProductHandler 用户端商品处理器
|
||||||
|
type UserProductHandler struct{}
|
||||||
|
|
||||||
|
// GetHomeData 获取首页数据(轮播图、商品分类和一级商品)
|
||||||
|
func (h *UserProductHandler) GetHomeData(c *gin.Context) {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 获取启用的轮播图,按排序排列
|
||||||
|
var banners []models.Banner
|
||||||
|
err := db.Where("status = ?", 1).Order("sort_order ASC, created_at DESC").Find(&banners).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询轮播图失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取启用的商品分类,按排序排列
|
||||||
|
var categories []models.ProductCategory
|
||||||
|
err = db.Where("status = ?", 1).Order("sort_order ASC, created_at DESC").Find(&categories).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品分类失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取上架的一级商品,按排序排列,限制数量
|
||||||
|
var primaryProducts []models.PrimaryProduct
|
||||||
|
err = db.Where("status = ?", 1).
|
||||||
|
Preload("Category").
|
||||||
|
Order("is_hot desc, sort_order ASC, created_at DESC").
|
||||||
|
Find(&primaryProducts).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询一级商品失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"banners": banners,
|
||||||
|
"categories": categories,
|
||||||
|
"primary_products": primaryProducts,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProductCategories 获取商品分类列表
|
||||||
|
func (h *UserProductHandler) GetProductCategories(c *gin.Context) {
|
||||||
|
var categories []models.ProductCategory
|
||||||
|
|
||||||
|
err := common.GetDB().Where("status = ?", 1).Order("sort_order ASC, created_at DESC").Find(&categories).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品分类失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrimaryProducts 获取一级商品列表
|
||||||
|
func (h *UserProductHandler) GetPrimaryProducts(c *gin.Context) {
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
categoryID := c.Query("category_id")
|
||||||
|
productName := c.Query("product_name")
|
||||||
|
sortType := c.DefaultQuery("sort", "default") // default, price_asc, price_desc, sales, views
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var products []models.PrimaryProduct
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件 - 只查询上架商品
|
||||||
|
query := db.Model(&models.PrimaryProduct{}).Where("status = ?", 1).Preload("Category")
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if categoryID != "" {
|
||||||
|
query = query.Where("category_id = ?", categoryID)
|
||||||
|
}
|
||||||
|
if productName != "" {
|
||||||
|
query = query.Where("product_name LIKE ?", "%"+productName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
switch sortType {
|
||||||
|
case "price_asc":
|
||||||
|
query = query.Order("current_price ASC")
|
||||||
|
case "price_desc":
|
||||||
|
query = query.Order("current_price DESC")
|
||||||
|
case "sales":
|
||||||
|
query = query.Order("sales_count DESC")
|
||||||
|
case "views":
|
||||||
|
query = query.Order("view_count DESC")
|
||||||
|
default:
|
||||||
|
query = query.Order("sort_order ASC, created_at DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Find(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": products,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrimaryProduct 获取一级商品详情
|
||||||
|
func (h *UserProductHandler) GetPrimaryProduct(c *gin.Context) {
|
||||||
|
productID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var product models.PrimaryProduct
|
||||||
|
err = common.GetDB().Where("status = ?", 1).Preload("Category").First(&product, uint(productID)).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在或已下架"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加浏览量
|
||||||
|
common.GetDB().Model(&product).UpdateColumn("view_count", product.ViewCount+1)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(product))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecondaryProducts 获取二级商品列表(用户挂售的商品)
|
||||||
|
func (h *UserProductHandler) GetSecondaryProducts(c *gin.Context) {
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
productName := c.Query("product_name")
|
||||||
|
sortType := c.DefaultQuery("sort", "default") // default, price_asc, price_desc, views
|
||||||
|
categoryId, _ := strconv.Atoi(c.DefaultQuery("category_id", "0"))
|
||||||
|
|
||||||
|
// 地址筛选参数
|
||||||
|
province := c.Query("province")
|
||||||
|
city := c.Query("city")
|
||||||
|
district := c.Query("district")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var products []models.SecondaryProduct
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件 - 只查询上架商品,并预加载卖家信息
|
||||||
|
query := db.Model(&models.SecondaryProduct{}).Where("status = ? and quantity>0", 1).
|
||||||
|
Preload("Seller")
|
||||||
|
|
||||||
|
// 添加商品名称搜索条件
|
||||||
|
if productName != "" {
|
||||||
|
query = query.Where("product_name LIKE ?", "%"+productName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分类筛选条件
|
||||||
|
if categoryId > 0 {
|
||||||
|
query = query.Where("category_id = ?", categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加地址筛选条件 - 根据卖家的默认地址筛选
|
||||||
|
if province != "" || city != "" || district != "" {
|
||||||
|
// 子查询获取符合地址条件的用户ID
|
||||||
|
subQuery := db.Model(&models.UserAddress{}).
|
||||||
|
Select("user_id").
|
||||||
|
Where("is_default = ?", 1)
|
||||||
|
|
||||||
|
if province != "" {
|
||||||
|
subQuery = subQuery.Where("province = ?", province)
|
||||||
|
}
|
||||||
|
if city != "" {
|
||||||
|
subQuery = subQuery.Where("city = ?", city)
|
||||||
|
}
|
||||||
|
if district != "" {
|
||||||
|
subQuery = subQuery.Where("district = ?", district)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Where("seller_id IN (?)", subQuery)
|
||||||
|
}
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
switch sortType {
|
||||||
|
case "price":
|
||||||
|
query = query.Order("selling_price ASC")
|
||||||
|
case "price_desc":
|
||||||
|
query = query.Order("selling_price DESC")
|
||||||
|
case "view":
|
||||||
|
query = query.Order("view_count DESC")
|
||||||
|
case "sales":
|
||||||
|
query = query.Order("sales_count DESC")
|
||||||
|
case "quantity":
|
||||||
|
query = query.Order("quantity DESC")
|
||||||
|
default:
|
||||||
|
query = query.Order("created_at DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Find(&products).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询商品列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
currentUser, exists := c.Get("current_user")
|
||||||
|
var userID uint
|
||||||
|
if exists {
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
userID = user.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取系统配置limit_days
|
||||||
|
var config models.SystemConfig
|
||||||
|
db.Where("config_key=?", "limit_days").First(&config)
|
||||||
|
limitDays, _ := strconv.Atoi(config.ConfigValue)
|
||||||
|
|
||||||
|
// 为每个商品添加购买限制检查
|
||||||
|
type ProductWithPurchaseLimit struct {
|
||||||
|
models.SecondaryProduct
|
||||||
|
CanPurchase bool `json:"can_purchase"`
|
||||||
|
PurchaseLimitMessage string `json:"purchase_limit_message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
productList := make([]ProductWithPurchaseLimit, 0, len(products))
|
||||||
|
for _, product := range products {
|
||||||
|
productWithLimit := ProductWithPurchaseLimit{
|
||||||
|
SecondaryProduct: product,
|
||||||
|
CanPurchase: true,
|
||||||
|
PurchaseLimitMessage: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户已登录,检查购买限制
|
||||||
|
if exists && userID > 0 {
|
||||||
|
// 检查是否是自己的商品
|
||||||
|
if product.SellerID == userID {
|
||||||
|
productWithLimit.CanPurchase = false
|
||||||
|
productWithLimit.PurchaseLimitMessage = "不能购买自己的商品"
|
||||||
|
} else if limitDays > 0 {
|
||||||
|
// 检查是否在限制天数内已经购买过该卖家的商品
|
||||||
|
limitDaysAgo := time.Now().AddDate(0, 0, -limitDays)
|
||||||
|
var existingOrderCount int64
|
||||||
|
db.Model(&models.PurchaseOrder{}).Where(
|
||||||
|
"buyer_id = ? AND seller_id = ? AND product_type = 2 AND created_at > ? AND order_status != 3",
|
||||||
|
userID, product.SellerID, limitDaysAgo,
|
||||||
|
).Count(&existingOrderCount)
|
||||||
|
|
||||||
|
if existingOrderCount > 0 {
|
||||||
|
productWithLimit.CanPurchase = false
|
||||||
|
productWithLimit.PurchaseLimitMessage = fmt.Sprintf("%v天内只能向同一个卖家购买一次商品", limitDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productList = append(productList, productWithLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": productList,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecondaryProduct 获取二级商品详情
|
||||||
|
func (h *UserProductHandler) GetSecondaryProduct(c *gin.Context) {
|
||||||
|
productID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var product models.SecondaryProduct
|
||||||
|
err = common.GetDB().Where("status = ?", 1).Preload("Seller").First(&product, uint(productID)).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "商品不存在或已下架"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加浏览量
|
||||||
|
common.GetDB().Model(&product).UpdateColumn("view_count", product.ViewCount+1)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(product))
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserReconciliationHandler 用户端对账管理处理器
|
||||||
|
type UserReconciliationHandler struct{}
|
||||||
|
|
||||||
|
// GetReconciliationStats 获取对账统计数据
|
||||||
|
func (h *UserReconciliationHandler) GetReconciliationStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 获取日期参数
|
||||||
|
startDate := c.Query("start_date")
|
||||||
|
endDate := c.Query("end_date")
|
||||||
|
|
||||||
|
// 如果没有传入日期,默认为今天
|
||||||
|
if startDate == "" || endDate == "" {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
startDate = today
|
||||||
|
endDate = today
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析日期
|
||||||
|
startTime, err := time.Parse("2006-01-02", startDate)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "开始日期格式错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endTime, err := time.Parse("2006-01-02", endDate)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "结束日期格式错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置时间范围
|
||||||
|
startTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, startTime.Location())
|
||||||
|
endTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 23, 59, 59, 999999999, endTime.Location())
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"primary_confirmed": h.getPrimaryMarketStats(user.ID, startTime, endTime, 2), // 已确认
|
||||||
|
"primary_pending": h.getPrimaryMarketStats(user.ID, startTime, endTime, 1), // 待确认
|
||||||
|
"secondary_buy_confirmed": h.getSecondaryBuyStats(user.ID, startTime, endTime, 2), // 二级市场买单已确认
|
||||||
|
"secondary_buy_pending": h.getSecondaryBuyStats(user.ID, startTime, endTime, 1), // 二级市场买单待确认
|
||||||
|
"secondary_sell_confirmed": h.getSecondarySellStats(user.ID, startTime, endTime, 2), // 二级市场卖单已确认
|
||||||
|
"secondary_sell_pending": h.getSecondarySellStats(user.ID, startTime, endTime, 1), // 二级市场卖单待确认
|
||||||
|
"warehouse_active": h.getWarehouseStats(user.ID, 1), // 寄售中
|
||||||
|
"warehouse_inactive": h.getWarehouseStats(user.ID, 0), // 未寄售
|
||||||
|
"score_change": h.getScoreChangeStats(user.ID, startTime, endTime), // 积分变更
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPrimaryMarketStats 获取一级市场统计数据
|
||||||
|
func (h *UserReconciliationHandler) getPrimaryMarketStats(userID uint, startTime, endTime time.Time, status int) map[string]interface{} {
|
||||||
|
var totalAmount float64
|
||||||
|
var totalQuantity int64
|
||||||
|
db := common.GetDB()
|
||||||
|
// 查询一级商品订单统计
|
||||||
|
db.Model(&models.PurchaseOrder{}).
|
||||||
|
Where("buyer_id = ? AND product_type = ? AND order_status = ? AND created_at BETWEEN ? AND ?",
|
||||||
|
userID, 1, status, startTime, endTime).
|
||||||
|
Select("COALESCE(SUM(total_amount), 0) as total_amount, COALESCE(SUM(quantity), 0) as total_quantity").
|
||||||
|
Row().Scan(&totalAmount, &totalQuantity)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"amount": totalAmount,
|
||||||
|
"quantity": totalQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSecondaryBuyStats 获取二级市场买单统计数据
|
||||||
|
func (h *UserReconciliationHandler) getSecondaryBuyStats(userID uint, startTime, endTime time.Time, status int) map[string]interface{} {
|
||||||
|
var totalAmount float64
|
||||||
|
var totalQuantity int64
|
||||||
|
db := common.GetDB()
|
||||||
|
// 查询二级商品买单统计
|
||||||
|
db.Model(&models.PurchaseOrder{}).
|
||||||
|
Where("buyer_id = ? AND product_type = ? AND order_status = ? AND created_at BETWEEN ? AND ?",
|
||||||
|
userID, 2, status, startTime, endTime).
|
||||||
|
Select("COALESCE(SUM(total_amount), 0) as total_amount, COALESCE(SUM(quantity), 0) as total_quantity").
|
||||||
|
Row().Scan(&totalAmount, &totalQuantity)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"amount": totalAmount,
|
||||||
|
"quantity": totalQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSecondarySellStats 获取二级市场卖单统计数据
|
||||||
|
func (h *UserReconciliationHandler) getSecondarySellStats(userID uint, startTime, endTime time.Time, status int) map[string]interface{} {
|
||||||
|
var totalAmount float64
|
||||||
|
var totalQuantity int64
|
||||||
|
db := common.GetDB()
|
||||||
|
// 查询二级商品卖单统计
|
||||||
|
db.Model(&models.SalesOrder{}).
|
||||||
|
Where("seller_id = ? AND order_status = ? AND created_at BETWEEN ? AND ?",
|
||||||
|
userID, status, startTime, endTime).
|
||||||
|
Select("COALESCE(SUM(total_amount), 0) as total_amount, COALESCE(SUM(quantity), 0) as total_quantity").
|
||||||
|
Row().Scan(&totalAmount, &totalQuantity)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"amount": totalAmount,
|
||||||
|
"quantity": totalQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWarehouseStats 获取仓库商品统计数据
|
||||||
|
func (h *UserReconciliationHandler) getWarehouseStats(userID uint, status int) map[string]interface{} {
|
||||||
|
var totalAmount float64
|
||||||
|
var totalQuantity int64
|
||||||
|
db := common.GetDB()
|
||||||
|
// 查询仓库商品统计
|
||||||
|
db.Model(&models.UserWarehouse{}).
|
||||||
|
Where("user_id = ? AND status = ?", userID, status).
|
||||||
|
Select("COALESCE(SUM(warehouse_price * quantity), 0) as total_amount, COALESCE(SUM(quantity), 0) as total_quantity").
|
||||||
|
Row().Scan(&totalAmount, &totalQuantity)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"amount": totalAmount,
|
||||||
|
"quantity": totalQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getScoreChangeStats 获取积分变更统计数据
|
||||||
|
func (h *UserReconciliationHandler) getScoreChangeStats(userID uint, startTime, endTime time.Time) map[string]interface{} {
|
||||||
|
var increaseScore int64
|
||||||
|
var decreaseScore int64
|
||||||
|
db := common.GetDB()
|
||||||
|
// 查询增加的积分
|
||||||
|
db.Model(&models.ScoreRecord{}).
|
||||||
|
Where("user_id = ? AND change_number > 0 AND created_at BETWEEN ? AND ?",
|
||||||
|
userID, startTime, endTime).
|
||||||
|
Select("COALESCE(SUM(change_number), 0)").
|
||||||
|
Row().Scan(&increaseScore)
|
||||||
|
|
||||||
|
// 查询减少的积分(取绝对值)
|
||||||
|
db.Model(&models.ScoreRecord{}).
|
||||||
|
Where("user_id = ? AND change_number < 0 AND created_at BETWEEN ? AND ?",
|
||||||
|
userID, startTime, endTime).
|
||||||
|
Select("COALESCE(ABS(SUM(change_number)), 0)").
|
||||||
|
Row().Scan(&decreaseScore)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"increase": increaseScore,
|
||||||
|
"decrease": decreaseScore,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserScoreHandler 用户端积分处理器
|
||||||
|
type UserScoreHandler struct{}
|
||||||
|
|
||||||
|
// GetMyScoreRecords 获取我的积分记录列表
|
||||||
|
func (h *UserScoreHandler) GetMyScoreRecords(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
changeType := c.Query("change_type") // all, positive, negative
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var records []models.ScoreRecord
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件 - 只查询当前用户的积分记录
|
||||||
|
query := db.Model(&models.ScoreRecord{}).Where("user_id = ?", user.ID)
|
||||||
|
|
||||||
|
// 添加积分变化类型筛选
|
||||||
|
if changeType == "positive" {
|
||||||
|
query = query.Where("change_number > 0")
|
||||||
|
} else if changeType == "negative" {
|
||||||
|
query = query.Where("change_number < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询积分记录失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算统计信息
|
||||||
|
var totalIncome int // 总收入积分
|
||||||
|
var totalExpense int // 总支出积分
|
||||||
|
var currentPoints int // 当前积分
|
||||||
|
|
||||||
|
currentPoints = user.CurrentPoints
|
||||||
|
|
||||||
|
// 统计总收入和总支出
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ? AND change_number > 0", user.ID).
|
||||||
|
Select("COALESCE(SUM(change_number), 0)").Scan(&totalIncome)
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ? AND change_number < 0", user.ID).
|
||||||
|
Select("COALESCE(SUM(ABS(change_number)), 0)").Scan(&totalExpense)
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": records,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"current_points": currentPoints,
|
||||||
|
"total_income": totalIncome,
|
||||||
|
"total_expense": totalExpense,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyScoreStats 获取我的积分统计信息
|
||||||
|
func (h *UserScoreHandler) GetMyScoreStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 当前积分
|
||||||
|
currentPoints := user.CurrentPoints
|
||||||
|
|
||||||
|
// 总收入积分
|
||||||
|
var totalIncome int
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ? AND change_number > 0", user.ID).
|
||||||
|
Select("COALESCE(SUM(change_number), 0)").Scan(&totalIncome)
|
||||||
|
|
||||||
|
// 总支出积分
|
||||||
|
var totalExpense int
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ? AND change_number < 0", user.ID).
|
||||||
|
Select("COALESCE(SUM(ABS(change_number)), 0)").Scan(&totalExpense)
|
||||||
|
|
||||||
|
// 今日获得积分
|
||||||
|
var todayIncome int
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ? AND change_number > 0 AND DATE(created_at) = CURDATE()", user.ID).
|
||||||
|
Select("COALESCE(SUM(change_number), 0)").Scan(&todayIncome)
|
||||||
|
|
||||||
|
// 本月获得积分
|
||||||
|
var monthIncome int
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ? AND change_number > 0 AND YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())", user.ID).
|
||||||
|
Select("COALESCE(SUM(change_number), 0)").Scan(&monthIncome)
|
||||||
|
|
||||||
|
// 最近的积分记录(5条)
|
||||||
|
var recentRecords []models.ScoreRecord
|
||||||
|
db.Model(&models.ScoreRecord{}).Where("user_id = ?", user.ID).
|
||||||
|
Order("created_at DESC").Limit(5).Find(&recentRecords)
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"current_points": currentPoints,
|
||||||
|
"total_income": totalIncome,
|
||||||
|
"total_expense": totalExpense,
|
||||||
|
"today_income": todayIncome,
|
||||||
|
"month_income": monthIncome,
|
||||||
|
"recent_records": recentRecords,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserWarehouseHandler 用户仓库处理器
|
||||||
|
type UserWarehouseHandler struct{}
|
||||||
|
|
||||||
|
// GetWarehouseList 获取用户仓库商品列表
|
||||||
|
func (h *UserWarehouseHandler) GetWarehouseList(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 50 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选参数
|
||||||
|
productType := c.Query("product_type")
|
||||||
|
status := c.Query("status")
|
||||||
|
productName := c.Query("product_name")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var warehouses []models.UserWarehouse
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
query := db.Model(&models.UserWarehouse{}).Where("user_id = ? and quantity>0", user.ID)
|
||||||
|
|
||||||
|
// 添加筛选条件
|
||||||
|
if productType != "" {
|
||||||
|
query = query.Where("product_type = ?", productType)
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
if productName != "" {
|
||||||
|
query = query.Where("product_name LIKE ?", "%"+productName+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&warehouses).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "查询仓库商品列表失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总价值
|
||||||
|
var totalValue float64
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? AND status = 1", user.ID).
|
||||||
|
Select("COALESCE(SUM(warehouse_price * quantity), 0)").Scan(&totalValue)
|
||||||
|
|
||||||
|
// 构建返回数据
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"list": warehouses,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"total_value": totalValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWarehouseItem 获取单个仓库商品详情
|
||||||
|
func (h *UserWarehouseHandler) GetWarehouseItem(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
itemID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var warehouse models.UserWarehouse
|
||||||
|
err = common.GetDB().Where("id = ? AND user_id = ?", itemID, user.ID).First(&warehouse).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "仓库商品不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(warehouse))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SellWarehouseItem 出售仓库商品
|
||||||
|
func (h *UserWarehouseHandler) SellWarehouseItem(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
itemID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "商品ID无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Quantity int `json:"quantity" binding:"required,min=1"`
|
||||||
|
Price float64 `json:"price" binding:"required,gt=0"`
|
||||||
|
Description string `json:"description" binding:"required"`
|
||||||
|
Condition string `json:"condition" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 查询仓库商品
|
||||||
|
var warehouse models.UserWarehouse
|
||||||
|
if err := tx.Where("id = ? AND user_id = ? AND status = 1", itemID, user.ID).First(&warehouse).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(404, "仓库商品不存在或已出售"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查库存数量
|
||||||
|
if warehouse.Quantity < req.Quantity {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(400, "库存数量不足"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建二级商品上架
|
||||||
|
secondaryProduct := models.SecondaryProduct{
|
||||||
|
SellerID: user.ID,
|
||||||
|
OriginalProductID: &warehouse.ProductID,
|
||||||
|
OriginalProductType: warehouse.ProductType,
|
||||||
|
ProductName: warehouse.ProductName,
|
||||||
|
ProductDescription: req.Description,
|
||||||
|
ProductImages: warehouse.ProductImages,
|
||||||
|
CostPrice: warehouse.PurchasePrice,
|
||||||
|
SellingPrice: req.Price,
|
||||||
|
Quantity: req.Quantity,
|
||||||
|
Status: 1, // 上架状态
|
||||||
|
ViewCount: 0,
|
||||||
|
CategoryID: 1, // 默认分类,可以后续优化
|
||||||
|
ProductCondition: req.Condition,
|
||||||
|
UserWarehouseID: uint(itemID),
|
||||||
|
}
|
||||||
|
|
||||||
|
if secondaryProduct.OriginalProductType == 1 {
|
||||||
|
var primaryProduct models.PrimaryProduct
|
||||||
|
err = tx.Where("id=?", secondaryProduct.OriginalProductID).First(&primaryProduct).Error
|
||||||
|
if err == nil {
|
||||||
|
secondaryProduct.CategoryID = primaryProduct.CategoryID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var sec models.SecondaryProduct
|
||||||
|
err = tx.Where("id=?", secondaryProduct.OriginalProductID).First(&sec).Error
|
||||||
|
if err == nil {
|
||||||
|
secondaryProduct.CategoryID = sec.CategoryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&secondaryProduct).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "创建出售商品失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新仓库商品数量
|
||||||
|
if warehouse.Quantity == req.Quantity {
|
||||||
|
// 全部出售,更新状态为已出售
|
||||||
|
if err := tx.Model(&warehouse).Updates(map[string]interface{}{
|
||||||
|
"quantity": 0,
|
||||||
|
"status": 0, // 已出售
|
||||||
|
}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新仓库商品状态失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 部分出售,减少数量
|
||||||
|
if err := tx.Model(&warehouse).Update("quantity", warehouse.Quantity-req.Quantity).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
c.JSON(http.StatusOK, common.Error(500, "更新仓库商品数量失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"message": "商品挂售成功",
|
||||||
|
"secondary_product": secondaryProduct,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWarehouseStats 获取仓库统计信息
|
||||||
|
func (h *UserWarehouseHandler) GetWarehouseStats(c *gin.Context) {
|
||||||
|
currentUser, _ := c.Get("current_user")
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 库存中商品数量
|
||||||
|
var stockCount int64
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? AND status = 1", user.ID).Count(&stockCount)
|
||||||
|
|
||||||
|
// 已出售商品数量
|
||||||
|
var soldCount int64
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? AND status = 0", user.ID).Count(&soldCount)
|
||||||
|
|
||||||
|
// 库存总价值
|
||||||
|
var totalValue float64
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? AND status = 1", user.ID).
|
||||||
|
Select("COALESCE(SUM(warehouse_price * quantity), 0)").Scan(&totalValue)
|
||||||
|
|
||||||
|
// 一级商品数量
|
||||||
|
var primaryCount int64
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? AND product_type = 1 AND status = 1", user.ID).Count(&primaryCount)
|
||||||
|
|
||||||
|
// 二级商品数量
|
||||||
|
var secondaryCount int64
|
||||||
|
db.Model(&models.UserWarehouse{}).Where("user_id = ? AND product_type = 2 AND status = 1", user.ID).Count(&secondaryCount)
|
||||||
|
|
||||||
|
stats["stock_count"] = stockCount
|
||||||
|
stats["sold_count"] = soldCount
|
||||||
|
stats["total_value"] = totalValue
|
||||||
|
stats["primary_count"] = primaryCount
|
||||||
|
stats["secondary_count"] = secondaryCount
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, common.Success(stats))
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
jwtutil "awesomeProject/pkg/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Whitelist 白名单路径(支持通配符)
|
||||||
|
var Whitelist = []string{
|
||||||
|
"/back/file/**",
|
||||||
|
"/back/login",
|
||||||
|
}
|
||||||
|
|
||||||
|
func CORSMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Methods", "*")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "*")
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWTMiddleware(c *gin.Context) {
|
||||||
|
// ✅ 1. 获取请求路径
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if !strings.HasPrefix(path, "/back") {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ✅ 2. 检查是否在白名单中
|
||||||
|
for _, rule := range Whitelist {
|
||||||
|
if PathMatch(rule, path) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 3. 从 Header 中提取 Token
|
||||||
|
tokenString := c.GetHeader("token")
|
||||||
|
if tokenString == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "token无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 4. 解析并验证 Token
|
||||||
|
claims, err := jwtutil.ParseTokenToMap(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "token无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 5. Token 有效,继续处理
|
||||||
|
c.Set("tokenMap", claims)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathMatch 判断请求路径是否匹配白名单中的通配符规则
|
||||||
|
func PathMatch(pattern, path string) bool {
|
||||||
|
if pattern == path {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pattern) > 0 && pattern[0] == '/' {
|
||||||
|
pattern = pattern[1:]
|
||||||
|
}
|
||||||
|
if len(path) > 0 && path[0] == '/' {
|
||||||
|
path = path[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
patternParts := strings.Split(pattern, "/")
|
||||||
|
pathParts := strings.Split(path, "/")
|
||||||
|
|
||||||
|
if len(patternParts) > len(pathParts) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(patternParts); i++ {
|
||||||
|
p := patternParts[i]
|
||||||
|
if p == "**" {
|
||||||
|
// 匹配任意多级路径
|
||||||
|
return true
|
||||||
|
} else if p == "*" {
|
||||||
|
// 匹配单级路径
|
||||||
|
if i == len(patternParts)-1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if p != pathParts[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果模式完全匹配路径的前缀
|
||||||
|
return len(patternParts) == len(pathParts)
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminRoleMiddleware 管理员权限中间件
|
||||||
|
func AdminRoleMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 获取token中的用户信息
|
||||||
|
tokenMap, exists := c.Get("tokenMap")
|
||||||
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "未授权访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := tokenMap.(map[string]interface{})
|
||||||
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户信息无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := uint(userIDFloat)
|
||||||
|
|
||||||
|
// 查询用户信息
|
||||||
|
var user models.User
|
||||||
|
if err := common.GetDB().First(&user, userID).Error; err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户角色:0管理员,1代理商,2普通用户
|
||||||
|
if user.SystemRole != 0 {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(403, "权限不足,仅管理员可访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("current_user", user)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentRoleMiddleware 代理商权限中间件
|
||||||
|
func AgentRoleMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 获取token中的用户信息
|
||||||
|
tokenMap, exists := c.Get("tokenMap")
|
||||||
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "未授权访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := tokenMap.(map[string]interface{})
|
||||||
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户信息无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := uint(userIDFloat)
|
||||||
|
|
||||||
|
// 查询用户信息
|
||||||
|
var user models.User
|
||||||
|
if err := common.GetDB().First(&user, userID).Error; err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户角色:0管理员,1代理商
|
||||||
|
if user.SystemRole != 0 && user.SystemRole != 1 {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(403, "权限不足,仅管理员和代理商可访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("current_user", user)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func UserRoleMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
|
// 获取token中的用户信息
|
||||||
|
tokenMap, exists := c.Get("tokenMap")
|
||||||
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "未授权访问"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := tokenMap.(map[string]interface{})
|
||||||
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户信息无效"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := uint(userIDFloat)
|
||||||
|
|
||||||
|
// 查询用户信息
|
||||||
|
var user models.User
|
||||||
|
if err := common.GetDB().First(&user, userID).Error; err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户不存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("current_user", user)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUserPermission 检查用户权限(代理商只能查看自己邀请的用户)
|
||||||
|
func CheckUserPermission() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
currentUser, exists := c.Get("current_user")
|
||||||
|
if !exists {
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(401, "用户信息获取失败"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
|
||||||
|
// 如果是管理员,拥有所有权限
|
||||||
|
if user.SystemRole == 0 {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是代理商,需要检查权限
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
// 获取请求路径
|
||||||
|
//path := c.Request.URL.Path
|
||||||
|
method := c.Request.Method
|
||||||
|
|
||||||
|
//// 代理商权限限制
|
||||||
|
//allowedPaths := []string{
|
||||||
|
// "/back/admin/users", // 查看用户列表
|
||||||
|
// "/back/admin/purchase-orders", // 查看买单
|
||||||
|
// "/back/admin/sales-orders", // 查看卖单
|
||||||
|
// "/back/admin/score-records", // 查看积分记录
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//// 检查是否为允许的路径
|
||||||
|
//pathAllowed := false
|
||||||
|
//for _, allowedPath := range allowedPaths {
|
||||||
|
// if strings.HasPrefix(path, allowedPath) {
|
||||||
|
// pathAllowed = true
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//if !pathAllowed {
|
||||||
|
// c.AbortWithStatusJSON(http.StatusOK, common.Error(403, "代理商权限不足"))
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
|
// 对于GET请求,添加查询过滤条件
|
||||||
|
if method == "GET" {
|
||||||
|
// 设置代理商身份码,用于数据过滤
|
||||||
|
c.Set("agent_identity_code", user.IdentityCode)
|
||||||
|
} else {
|
||||||
|
// 非GET请求,代理商不允许
|
||||||
|
c.AbortWithStatusJSON(http.StatusOK, common.Error(403, "代理商只能查看数据,不能进行修改操作"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIDFromParam 从URL参数中获取用户ID并检查权限
|
||||||
|
func GetUserIDFromParam(c *gin.Context, paramName string) (uint, error) {
|
||||||
|
currentUser, exists := c.Get("current_user")
|
||||||
|
if !exists {
|
||||||
|
return 0, fmt.Errorf("用户信息获取失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := currentUser.(models.User)
|
||||||
|
userIDStr := c.Param(paramName)
|
||||||
|
userID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("用户ID无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUserID := uint(userID)
|
||||||
|
|
||||||
|
// 如果是管理员,可以查看所有用户
|
||||||
|
if user.SystemRole == 0 {
|
||||||
|
return targetUserID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是代理商,只能查看自己邀请的用户
|
||||||
|
if user.SystemRole == 1 {
|
||||||
|
var targetUser models.User
|
||||||
|
if err := common.GetDB().First(&targetUser, targetUserID).Error; err != nil {
|
||||||
|
return 0, fmt.Errorf("目标用户不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为代理商邀请的用户
|
||||||
|
if targetUser.ReferrerIdentityCode != user.IdentityCode {
|
||||||
|
return 0, fmt.Errorf("权限不足,只能查看您邀请的用户")
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetUserID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("权限不足")
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Banner 轮播图表
|
||||||
|
type Banner struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
Title string `gorm:"type:varchar(200);not null;comment:轮播图标题" json:"title" binding:"required"`
|
||||||
|
ImageUrl string `gorm:"type:varchar(500);not null;comment:轮播图图片URL" json:"image_url" binding:"required"`
|
||||||
|
LinkUrl string `gorm:"type:varchar(500);comment:点击跳转链接" json:"link_url"`
|
||||||
|
Description string `gorm:"type:text;comment:描述信息" json:"description"`
|
||||||
|
SortOrder int `gorm:"type:int;not null;default:0;comment:排序序号" json:"sort_order"`
|
||||||
|
Status uint8 `gorm:"type:tinyint;not null;default:1;comment:状态:0禁用,1启用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Banner) TableName() string {
|
||||||
|
return "banners"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AutoMigrateAll 自动迁移所有数据表
|
||||||
|
func AutoMigrateAll() error {
|
||||||
|
db := common.GetDB()
|
||||||
|
|
||||||
|
// 按依赖关系迁移表
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&User{},
|
||||||
|
&UserAddress{},
|
||||||
|
&ProductCategory{},
|
||||||
|
&PrimaryProduct{},
|
||||||
|
&SecondaryProduct{},
|
||||||
|
&UserWarehouse{},
|
||||||
|
&PurchaseOrder{},
|
||||||
|
&SalesOrder{},
|
||||||
|
&OrderMessage{}, // 订单留言表
|
||||||
|
&SystemConfig{},
|
||||||
|
&ScoreRecord{},
|
||||||
|
&Banner{}, // 轮播图表
|
||||||
|
&config.ConfigChange{}, // 配置变更记录表
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OrderMessage 订单留言模型
|
||||||
|
type OrderMessage struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
OrderID uint `gorm:"not null;index" json:"order_id"` // 订单ID
|
||||||
|
OrderType string `gorm:"type:varchar(20);not null;default:'purchase'" json:"order_type"` // 订单类型:purchase(买单), sales(卖单)
|
||||||
|
UserID uint `gorm:"not null;index" json:"user_id"` // 留言用户ID
|
||||||
|
Content string `gorm:"type:text;not null" json:"content"` // 留言内容
|
||||||
|
Images string `gorm:"type:text" json:"images"` // 留言图片,多个图片用逗号分隔
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (OrderMessage) TableName() string {
|
||||||
|
return "order_messages"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrimaryProduct 一级商品表(管理员发布的官方商品)
|
||||||
|
type PrimaryProduct struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
CategoryID uint `gorm:"type:int;not null;comment:分类ID" json:"category_id" binding:"required"`
|
||||||
|
ProductName string `gorm:"type:varchar(200);not null;comment:商品名称" json:"product_name" binding:"required"`
|
||||||
|
ProductDescription string `gorm:"type:text;comment:商品描述" json:"product_description"`
|
||||||
|
ProductImages string `gorm:"type:text;comment:商品图片URLs,逗号分割" json:"product_images"`
|
||||||
|
OriginalPrice float64 `gorm:"type:decimal(10,2);not null;comment:原价" json:"original_price" binding:"required"`
|
||||||
|
CurrentPrice float64 `gorm:"type:decimal(10,2);not null;comment:现价" json:"current_price" binding:"required"`
|
||||||
|
StockQuantity int `gorm:"type:int;not null;default:0;comment:库存数量" json:"stock_quantity"`
|
||||||
|
SalesCount int `gorm:"type:int;not null;default:0;comment:销量" json:"sales_count"`
|
||||||
|
ViewCount int `gorm:"type:int;not null;default:0;comment:浏览量" json:"view_count"`
|
||||||
|
Status uint8 `gorm:"type:tinyint;not null;default:1;comment:状态:0下架,1上架" json:"status"`
|
||||||
|
IsHot int8 `gorm:"type:tinyint;not null;default:0;comment:是否为热门商品:0否,1是" json:"is_hot"`
|
||||||
|
SortOrder int `gorm:"type:int;not null;default:0;comment:排序序号" json:"sort_order"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
|
||||||
|
// 关联关系 - 注意:不要在关联字段上加验证标签
|
||||||
|
Category ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PrimaryProduct) TableName() string {
|
||||||
|
return "primary_products"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProductCategory 商品分类表
|
||||||
|
type ProductCategory struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
CategoryName string `gorm:"type:varchar(100);not null;comment:分类名称" json:"category_name" binding:"required"`
|
||||||
|
CategoryIcon string `gorm:"type:varchar(255);comment:分类图标URL" json:"category_icon"`
|
||||||
|
SortOrder int `gorm:"type:int;not null;default:0;comment:排序序号" json:"sort_order"`
|
||||||
|
Status uint8 `gorm:"type:tinyint;not null;default:1;comment:状态:0禁用,1启用" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ProductCategory) TableName() string {
|
||||||
|
return "product_categories"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PurchaseOrder 买单表
|
||||||
|
type PurchaseOrder struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
OrderNo string `gorm:"type:varchar(50);not null;uniqueIndex;comment:订单编号" json:"order_no"`
|
||||||
|
BuyerID uint `gorm:"type:int;not null;comment:买家用户ID" json:"buyer_id" binding:"required"`
|
||||||
|
SellerID *uint `gorm:"type:int;comment:卖家用户ID(二级商品交易时使用)" json:"seller_id"`
|
||||||
|
ProductID uint `gorm:"type:int;not null;comment:商品ID" json:"product_id" binding:"required"`
|
||||||
|
ProductType uint8 `gorm:"type:tinyint;not null;comment:商品类型:1一级商品,2二级商品" json:"product_type" binding:"required"`
|
||||||
|
ProductName string `gorm:"type:varchar(200);not null;comment:商品名称" json:"product_name" binding:"required"`
|
||||||
|
ProductImages string `gorm:"type:text;comment:商品图片URLs,逗号分割" json:"product_images"`
|
||||||
|
UnitPrice float64 `gorm:"type:decimal(10,2);not null;comment:单价" json:"unit_price" binding:"required"`
|
||||||
|
Quantity int `gorm:"type:int;not null;default:1;comment:数量" json:"quantity"`
|
||||||
|
TotalAmount float64 `gorm:"type:decimal(10,2);not null;comment:总金额" json:"total_amount" binding:"required"`
|
||||||
|
AddressID *uint `gorm:"type:int;comment:收货地址ID" json:"address_id"`
|
||||||
|
OrderStatus uint8 `gorm:"type:tinyint;not null;default:0;comment:订单状态:0待付款,1待确认,2已完成,3已取消" json:"order_status"`
|
||||||
|
ConfirmTime *time.Time `gorm:"type:timestamp;null;comment:确认时间" json:"confirm_time"`
|
||||||
|
Remark string `gorm:"type:text;comment:备注" json:"remark"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
PaymentProofImage string `gorm:"type:varchar(255)" json:"payment_proof_image"`
|
||||||
|
// 关联关系
|
||||||
|
Buyer User `gorm:"foreignKey:BuyerID" json:"buyer,omitempty"`
|
||||||
|
Seller *User `gorm:"foreignKey:SellerID" json:"seller,omitempty"`
|
||||||
|
Address *UserAddress `gorm:"foreignKey:AddressID" json:"address,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseOrder) TableName() string {
|
||||||
|
return "purchase_orders"
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SalesOrder 卖单表
|
||||||
|
type SalesOrder struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
OrderNo string `gorm:"type:varchar(50);not null;uniqueIndex;comment:订单编号" json:"order_no"`
|
||||||
|
SellerID uint `gorm:"type:int;not null;comment:卖家用户ID" json:"seller_id" binding:"required"`
|
||||||
|
BuyerID uint `gorm:"type:int;not null;comment:买家用户ID" json:"buyer_id" binding:"required"`
|
||||||
|
SecondaryProductID uint `gorm:"type:int;not null;comment:二级商品ID" json:"secondary_product_id" binding:"required"`
|
||||||
|
ProductName string `gorm:"type:varchar(200);not null;comment:商品名称" json:"product_name" binding:"required"`
|
||||||
|
ProductImages string `gorm:"type:text;comment:商品图片URLs,逗号分割" json:"product_images"`
|
||||||
|
SellingPrice float64 `gorm:"type:decimal(10,2);not null;comment:出售价格" json:"selling_price" binding:"required"`
|
||||||
|
Quantity int `gorm:"type:int;not null;default:1;comment:数量" json:"quantity"`
|
||||||
|
TotalAmount float64 `gorm:"type:decimal(10,2);not null;comment:总金额" json:"total_amount" binding:"required"`
|
||||||
|
AddressID *uint `gorm:"type:int;comment:收货地址ID" json:"address_id"`
|
||||||
|
OrderStatus uint8 `gorm:"type:tinyint;not null;default:0;comment:订单状态:0待付款,1待发货,2确认完成,3已取消" json:"order_status"`
|
||||||
|
CompleteTime *time.Time `gorm:"type:timestamp;null;comment:完成时间" json:"complete_time"`
|
||||||
|
Remark string `gorm:"type:text;comment:备注" json:"remark"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
PaymentProofImage string `json:"payment_proof_image" gorm:"type:varchar(255)"`
|
||||||
|
PurchaseOrderID uint `gorm:"type:int;" json:"purchase_order_id"`
|
||||||
|
// 关联关系
|
||||||
|
Seller *User `gorm:"foreignKey:SellerID" json:"seller,omitempty"`
|
||||||
|
Buyer User `gorm:"foreignKey:BuyerID" json:"buyer,omitempty"`
|
||||||
|
SecondaryProduct SecondaryProduct `gorm:"foreignKey:SecondaryProductID" json:"secondary_product,omitempty"`
|
||||||
|
Address *UserAddress `gorm:"foreignKey:AddressID" json:"address,omitempty"`
|
||||||
|
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SalesOrder) TableName() string {
|
||||||
|
return "sales_orders"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScoreRecord 积分记录表
|
||||||
|
type ScoreRecord struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
UserID uint `gorm:"type:int;not null;comment:用户编号" json:"user_id" binding:"required"`
|
||||||
|
ChangeNumber int `gorm:"type:int;not null;comment:变化数量" json:"change_number" binding:"required"`
|
||||||
|
Note string `gorm:"type:varchar(200);not null;comment:说明" json:"note" binding:"required"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ScoreRecord) TableName() string {
|
||||||
|
return "score_records"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecondaryProduct 二级商品表(用户上架的商品)
|
||||||
|
type SecondaryProduct struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
SellerID uint `gorm:"type:int;not null;comment:卖家用户ID" json:"seller_id" binding:"required"`
|
||||||
|
OriginalProductID *uint `gorm:"type:int;comment:原始商品ID(一级商品ID或二级商品ID)" json:"original_product_id"`
|
||||||
|
OriginalProductType uint8 `gorm:"type:tinyint;not null;comment:原始商品类型:1一级商品,2二级商品" json:"original_product_type" binding:"required"`
|
||||||
|
ProductName string `gorm:"type:varchar(200);not null;comment:商品名称" json:"product_name" binding:"required"`
|
||||||
|
ProductDescription string `gorm:"type:text;comment:商品描述" json:"product_description"`
|
||||||
|
ProductImages string `gorm:"type:text;comment:商品图片URLs,JSON格式存储" json:"product_images"`
|
||||||
|
CostPrice float64 `gorm:"type:decimal(10,2);not null;comment:成本价(用户购买时的价格)" json:"cost_price" binding:"required"`
|
||||||
|
SellingPrice float64 `gorm:"type:decimal(10,2);not null;comment:出售价格" json:"selling_price" binding:"required"`
|
||||||
|
Quantity int `gorm:"type:int;not null;default:1;comment:出售数量" json:"quantity"`
|
||||||
|
Status uint8 `gorm:"type:tinyint;not null;default:1;comment:状态:0下架,1上架,2已售出" json:"status"`
|
||||||
|
ViewCount int `gorm:"type:int;not null;default:0;comment:浏览量" json:"view_count"`
|
||||||
|
SalesCount int `gorm:"type:int;not null;default:0;" json:"sales_count"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
CategoryID uint `gorm:"type:int;not null;default:0;comment:商品分类ID" json:"category_id"`
|
||||||
|
ProductCondition string `gorm:"type varchar(255)" json:"product_condition"`
|
||||||
|
UserWarehouseID uint `gorm:"type:int;not null" json:"user_warehouse_id"`
|
||||||
|
// 关联关系
|
||||||
|
Seller User `gorm:"foreignKey:SellerID" json:"seller,omitempty"`
|
||||||
|
Category *ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
|
UserWarehouse *UserWarehouse `gorm:"foreignKey:UserWarehouseID" json:"user_warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SecondaryProduct) TableName() string {
|
||||||
|
return "secondary_products"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SystemConfig 系统常量设置表
|
||||||
|
type SystemConfig struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
ConfigKey string `gorm:"type:varchar(100);not null;uniqueIndex;comment:配置键" json:"config_key" binding:"required"`
|
||||||
|
ConfigValue string `gorm:"type:text;not null;comment:配置值" json:"config_value" binding:"required"`
|
||||||
|
ConfigName string `gorm:"type:varchar(200);not null;comment:配置名称" json:"config_name" binding:"required"`
|
||||||
|
ConfigDescription string `gorm:"type:text;comment:配置描述" json:"config_description"`
|
||||||
|
ConfigType string `gorm:"type:varchar(20);not null;default:'string';comment:配置类型:string、number、boolean、json" json:"config_type"`
|
||||||
|
IsSystem uint8 `gorm:"type:tinyint;not null;default:0;comment:是否系统配置:0否,1是" json:"is_system"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SystemConfig) TableName() string {
|
||||||
|
return "system_configs"
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User 用户表
|
||||||
|
type User struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
Phone string `gorm:"type:varchar(20);not null;uniqueIndex" json:"phone" binding:"required"`
|
||||||
|
SystemRole uint8 `gorm:"type:tinyint;not null;default:2;comment:系统角色:0管理员,1代理商,2普通用户" json:"system_role"`
|
||||||
|
CustomerName string `gorm:"type:varchar(50)" json:"customer_name"`
|
||||||
|
RealName string `gorm:"type:varchar(50)" json:"real_name"`
|
||||||
|
CurrentPoints int `gorm:"type:int;not null;default:0;comment:当前积分" json:"current_points"`
|
||||||
|
Avatar string `gorm:"type:varchar(255)" json:"avatar"`
|
||||||
|
CompanyName string `gorm:"type:varchar(100)" json:"company_name"`
|
||||||
|
PersonalIntro string `gorm:"type:text" json:"personal_intro"`
|
||||||
|
IdentityCode string `gorm:"type:varchar(50);uniqueIndex" json:"identity_code"`
|
||||||
|
ReferrerIdentityCode string `gorm:"type:varchar(50)" json:"referrer_identity_code"`
|
||||||
|
BusinessLicenseImage string `gorm:"type:varchar(255)" json:"business_license_image"`
|
||||||
|
IdCardFrontImage string `gorm:"type:varchar(255)" json:"id_card_front_image"`
|
||||||
|
IdCardBackImage string `gorm:"type:varchar(255)" json:"id_card_back_image"`
|
||||||
|
MainPaymentQrImage *string `gorm:"type:varchar(255)" json:"main_payment_qr_image"`
|
||||||
|
SubPaymentQrImage *string `gorm:"type:varchar(255)" json:"sub_payment_qr_image"`
|
||||||
|
Password string `gorm:"type:varchar(255)" json:"password"`
|
||||||
|
Status uint8 `gorm:"type:tinyint;not null;default:1;comment:状态:0禁用,1启用" json:"status"`
|
||||||
|
ExpiryDate *time.Time `gorm:"type:timestamp;null;comment:过期时间,null表示永不过期" json:"expiry_date"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAddress 收货地址表
|
||||||
|
type UserAddress struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
UserID uint `gorm:"type:int;not null;comment:用户ID" json:"user_id"`
|
||||||
|
ConsigneeName string `gorm:"type:varchar(50);not null;comment:收件人姓名" json:"consignee_name" binding:"required"`
|
||||||
|
ConsigneePhone string `gorm:"type:varchar(20);not null;comment:收件人电话" json:"consignee_phone" binding:"required"`
|
||||||
|
Province string `gorm:"type:varchar(50);not null;comment:省份" json:"province" binding:"required"`
|
||||||
|
City string `gorm:"type:varchar(50);not null;comment:城市" json:"city" binding:"required"`
|
||||||
|
District string `gorm:"type:varchar(50);not null;comment:区县" json:"district" binding:"required"`
|
||||||
|
DetailAddress string `gorm:"type:varchar(200);not null;comment:详细地址" json:"detail_address" binding:"required"`
|
||||||
|
PostalCode string `gorm:"type:varchar(10);comment:邮政编码" json:"postal_code"`
|
||||||
|
IsDefault uint8 `gorm:"type:tinyint;not null;default:0;comment:是否默认地址:0否,1是" json:"is_default"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UserAddress) TableName() string {
|
||||||
|
return "user_addresses"
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserWarehouse 用户仓库表
|
||||||
|
type UserWarehouse struct {
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
UserID uint `gorm:"type:int;not null;comment:用户ID" json:"user_id" binding:"required"`
|
||||||
|
ProductID uint `gorm:"type:int;not null;comment:商品ID" json:"product_id" binding:"required"`
|
||||||
|
ProductType uint8 `gorm:"type:tinyint;not null;comment:商品类型:1一级商品,2二级商品" json:"product_type" binding:"required"`
|
||||||
|
ProductName string `gorm:"type:varchar(200);not null;comment:商品名称" json:"product_name" binding:"required"`
|
||||||
|
ProductImages string `gorm:"type:text;comment:商品图片URLs,JSON格式存储" json:"product_images"`
|
||||||
|
PurchasePrice float64 `gorm:"type:decimal(10,2);not null;comment:购买价格" json:"purchase_price" binding:"required"`
|
||||||
|
WarehousePrice float64 `gorm:"type:decimal(10,2);not null;comment:入库价格(根据系统常量增值后)" json:"warehouse_price" binding:"required"`
|
||||||
|
Quantity int `gorm:"type:int;not null;default:1;comment:数量" json:"quantity"`
|
||||||
|
SourceOrderID *uint `gorm:"type:int;comment:来源订单ID" json:"source_order_id"`
|
||||||
|
Status uint8 `gorm:"type:tinyint;not null;default:1;comment:状态:0已出售,1库存中" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UserWarehouse) TableName() string {
|
||||||
|
return "user_warehouse"
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/handlers"
|
||||||
|
"awesomeProject/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupAdminRoutes 设置管理端路由
|
||||||
|
func SetupAdminRoutes(router *gin.Engine) {
|
||||||
|
// 管理端路由组
|
||||||
|
adminGroup := router.Group("/back/admin")
|
||||||
|
|
||||||
|
// 应用JWT中间件
|
||||||
|
adminGroup.Use(middleware.JWTMiddleware)
|
||||||
|
|
||||||
|
// 创建handler实例
|
||||||
|
userHandler := &handlers.AdminUserHandler{}
|
||||||
|
orderHandler := &handlers.AdminOrderHandler{}
|
||||||
|
scoreHandler := &handlers.AdminScoreHandler{}
|
||||||
|
productHandler := &handlers.AdminProductHandler{}
|
||||||
|
configHandler := &handlers.AdminConfigHandler{}
|
||||||
|
paymentHandler := &handlers.AdminPaymentHandler{}
|
||||||
|
orderMessageHandler := &handlers.AdminOrderMessageHandler{}
|
||||||
|
bannerHandler := &handlers.AdminBannerHandler{}
|
||||||
|
|
||||||
|
// 用户管理路由(管理员和代理商都可访问,但代理商有限制)
|
||||||
|
userRoutes := adminGroup.Group("/users")
|
||||||
|
|
||||||
|
userRoutes.Use(middleware.AgentRoleMiddleware(), middleware.CheckUserPermission())
|
||||||
|
{
|
||||||
|
userRoutes.GET("", userHandler.GetUsers) // 获取用户列表
|
||||||
|
|
||||||
|
userRoutes.GET("/:id", userHandler.GetUser) // 获取单个用户信息
|
||||||
|
userRoutes.GET("/stats", userHandler.GetUserStats) // 获取用户统计
|
||||||
|
userRoutes.GET("/search-stats", userHandler.SearchUserStats) // 搜索用户统计
|
||||||
|
// 以下接口仅管理员可访问
|
||||||
|
userRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
|
||||||
|
userRoutes.POST("", userHandler.CreateUser) // 创建用户
|
||||||
|
userRoutes.PUT("/:id", userHandler.UpdateUser) // 更新用户
|
||||||
|
userRoutes.PUT("/:id/expiry", userHandler.SetUserExpiryDate) // 设置用户过期时间
|
||||||
|
userRoutes.DELETE("/:id", userHandler.DeleteUser) // 删除用户
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户库存管理路由(仅管理员可访问)
|
||||||
|
warehouseRoutes := adminGroup.Group("/warehouse")
|
||||||
|
warehouseRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
{
|
||||||
|
warehouseRoutes.GET("/users", userHandler.GetAllUserWarehouses) // 获取所有用户库存汇总
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单管理路由(管理员和代理商都可访问,但代理商有限制)
|
||||||
|
orderRoutes := adminGroup.Group("/orders")
|
||||||
|
orderRoutes.Use(middleware.AgentRoleMiddleware(), middleware.CheckUserPermission())
|
||||||
|
{
|
||||||
|
// 买单管理
|
||||||
|
purchaseGroup := orderRoutes.Group("/purchase")
|
||||||
|
{
|
||||||
|
purchaseGroup.GET("", orderHandler.GetPurchaseOrders) // 获取买单列表
|
||||||
|
purchaseGroup.GET("/stats", orderHandler.GetOrderStats) // 获取订单统计
|
||||||
|
purchaseGroup.GET("/trend", orderHandler.GetOrderTrend) // 获取订单趋势
|
||||||
|
purchaseGroup.GET("/activities", orderHandler.GetRecentActivities) // 获取最近活动
|
||||||
|
purchaseGroup.GET("/:id", orderHandler.GetPurchaseOrder) // 获取单个买单详情
|
||||||
|
|
||||||
|
// 以下接口仅管理员可访问
|
||||||
|
purchaseGroup.Use(middleware.AdminRoleMiddleware())
|
||||||
|
purchaseGroup.PUT("/:id/status", orderHandler.UpdatePurchaseOrderStatus) // 更新买单状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卖单管理
|
||||||
|
salesGroup := orderRoutes.Group("/sales")
|
||||||
|
{
|
||||||
|
salesGroup.GET("", orderHandler.GetSalesOrders) // 获取卖单列表
|
||||||
|
salesGroup.GET("/:id", orderHandler.GetSalesOrder) // 获取单个卖单详情
|
||||||
|
|
||||||
|
// 以下接口仅管理员可访问
|
||||||
|
salesGroup.Use(middleware.AdminRoleMiddleware())
|
||||||
|
salesGroup.PUT("/:id/status", orderHandler.UpdateSalesOrderStatus) // 更新卖单状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单留言管理(管理员和代理商都可访问)
|
||||||
|
orderRoutes.GET("/:order_id/messages", orderMessageHandler.GetOrderMessages) // 获取订单留言列表
|
||||||
|
orderRoutes.POST("/:order_id/messages", orderMessageHandler.CreateOrderMessage) // 创建订单留言
|
||||||
|
}
|
||||||
|
|
||||||
|
// 积分管理路由(管理员和代理商都可访问,但代理商有限制)
|
||||||
|
scoreRoutes := adminGroup.Group("/scores")
|
||||||
|
scoreRoutes.Use(middleware.AgentRoleMiddleware(), middleware.CheckUserPermission())
|
||||||
|
{
|
||||||
|
scoreRoutes.GET("", scoreHandler.GetScoreRecords) // 获取积分记录列表
|
||||||
|
scoreRoutes.GET("/stats", scoreHandler.GetScoreStats) // 获取积分统计
|
||||||
|
scoreRoutes.GET("/:id", scoreHandler.GetScoreRecord) // 获取单个积分记录详情
|
||||||
|
|
||||||
|
// 以下接口仅管理员可访问
|
||||||
|
scoreRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
scoreRoutes.POST("", scoreHandler.CreateScoreRecord) // 创建积分记录
|
||||||
|
scoreRoutes.PUT("/:id", scoreHandler.UpdateScoreRecord) // 更新积分记录
|
||||||
|
scoreRoutes.DELETE("/:id", scoreHandler.DeleteScoreRecord) // 删除积分记录
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品管理路由(仅管理员可访问)
|
||||||
|
productRoutes := adminGroup.Group("/products")
|
||||||
|
productRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
{
|
||||||
|
// 商品分类管理
|
||||||
|
categoryGroup := productRoutes.Group("/categories")
|
||||||
|
{
|
||||||
|
categoryGroup.GET("", productHandler.GetProductCategories) // 获取分类列表
|
||||||
|
categoryGroup.POST("", productHandler.CreateProductCategory) // 创建分类
|
||||||
|
categoryGroup.PUT("/:id", productHandler.UpdateProductCategory) // 更新分类
|
||||||
|
categoryGroup.DELETE("/:id", productHandler.DeleteProductCategory) // 删除分类
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一级商品管理
|
||||||
|
primaryGroup := productRoutes.Group("/primary")
|
||||||
|
{
|
||||||
|
primaryGroup.GET("", productHandler.GetPrimaryProducts) // 获取一级商品列表
|
||||||
|
primaryGroup.GET("/stats", productHandler.GetProductStats) // 获取商品统计
|
||||||
|
primaryGroup.GET("/:id", productHandler.GetPrimaryProduct) // 获取单个一级商品详情
|
||||||
|
primaryGroup.POST("", productHandler.CreatePrimaryProduct) // 创建一级商品
|
||||||
|
primaryGroup.PUT("/:id", productHandler.UpdatePrimaryProduct) // 更新一级商品
|
||||||
|
primaryGroup.DELETE("/:id", productHandler.DeletePrimaryProduct) // 删除一级商品
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级商品管理(仅查看)
|
||||||
|
secondaryGroup := productRoutes.Group("/secondary")
|
||||||
|
{
|
||||||
|
secondaryGroup.GET("", productHandler.GetSecondaryProducts) // 获取二级商品列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统配置管理路由(仅管理员可访问)
|
||||||
|
configRoutes := adminGroup.Group("/configs")
|
||||||
|
configRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
{
|
||||||
|
configRoutes.GET("", configHandler.GetSystemConfigs) // 获取系统配置列表
|
||||||
|
configRoutes.GET("/:id", configHandler.GetSystemConfig) // 获取单个系统配置
|
||||||
|
configRoutes.POST("", configHandler.CreateSystemConfig) // 创建系统配置
|
||||||
|
configRoutes.PUT("/:id", configHandler.UpdateSystemConfig) // 更新系统配置
|
||||||
|
configRoutes.DELETE("/:id", configHandler.DeleteSystemConfig) // 删除系统配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员付款方式路由
|
||||||
|
paymentRoutes := adminGroup.Group("/payment")
|
||||||
|
{
|
||||||
|
paymentRoutes.GET("/info", paymentHandler.GetPaymentInfo) // 获取管理员付款方式信息(无需权限验证,用户购买时需要查看)
|
||||||
|
|
||||||
|
// 以下接口仅管理员可访问
|
||||||
|
paymentRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
paymentRoutes.PUT("/info", paymentHandler.UpdatePaymentInfo) // 更新管理员付款方式信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮播图管理路由(仅管理员可访问)
|
||||||
|
bannerRoutes := adminGroup.Group("/banners")
|
||||||
|
bannerRoutes.Use(middleware.AdminRoleMiddleware())
|
||||||
|
{
|
||||||
|
bannerRoutes.GET("", bannerHandler.GetBanners) // 获取轮播图列表
|
||||||
|
bannerRoutes.GET("/:id", bannerHandler.GetBanner) // 获取单个轮播图
|
||||||
|
bannerRoutes.POST("", bannerHandler.CreateBanner) // 创建轮播图
|
||||||
|
bannerRoutes.PUT("/:id", bannerHandler.UpdateBanner) // 更新轮播图
|
||||||
|
bannerRoutes.DELETE("/:id", bannerHandler.DeleteBanner) // 删除轮播图
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公共配置查询路由(根据key获取配置,无需权限验证)
|
||||||
|
adminGroup.GET("/config/:key", configHandler.GetConfigByKey)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/handlers"
|
||||||
|
"awesomeProject/internal/middleware"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupPublicRoutes 设置公共路由
|
||||||
|
func SetupPublicRoutes(router *gin.Engine) {
|
||||||
|
// 创建handler实例
|
||||||
|
authHandler := &handlers.AuthHandler{}
|
||||||
|
uploadHandler := handlers.NewUploadHandler()
|
||||||
|
|
||||||
|
// 公共路由组
|
||||||
|
publicGroup := router.Group("/back")
|
||||||
|
publicGroup.POST("/upload", uploadHandler.UploadFile)
|
||||||
|
publicGroup.GET("/config", func(context *gin.Context) {
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
key := context.DefaultQuery("key", "")
|
||||||
|
if key == "" {
|
||||||
|
context.JSON(200, common.Error(500, "键值对为空"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var config models.SystemConfig
|
||||||
|
db.Where("config_key=?", key).First(&config)
|
||||||
|
context.JSON(200, common.Success(config.ConfigValue))
|
||||||
|
})
|
||||||
|
// 认证相关路由(无需token验证)
|
||||||
|
authGroup := publicGroup.Group("/auth")
|
||||||
|
{
|
||||||
|
authGroup.POST("/login", authHandler.Login) // 用户登录
|
||||||
|
authGroup.POST("/register", authHandler.Register) // 用户注册
|
||||||
|
authGroup.POST("/send-sms", authHandler.SendSMS) // 发送短信验证码
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要token验证的公共路由
|
||||||
|
protectedGroup := publicGroup.Group("")
|
||||||
|
|
||||||
|
protectedGroup.Use(middleware.JWTMiddleware)
|
||||||
|
{
|
||||||
|
protectedGroup.GET("/current-user", authHandler.GetCurrentUser) // 获取当前用户信息
|
||||||
|
protectedGroup.PUT("/update-profile", authHandler.UpdateProfile) // 更新个人信息
|
||||||
|
protectedGroup.PUT("/change-password", authHandler.ChangePassword) // 修改密码
|
||||||
|
|
||||||
|
// 文件上传路由
|
||||||
|
// 单文件上传
|
||||||
|
protectedGroup.POST("/upload/batch", uploadHandler.BatchUploadFiles) // 批量文件上传
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupRouter 设置路由
|
||||||
|
func SetupRouter() *gin.Engine {
|
||||||
|
// 创建gin引擎
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
// 添加全局中间件
|
||||||
|
router.Use(middleware.CORSMiddleware())
|
||||||
|
|
||||||
|
// 设置公共路由(如登录、注册等)
|
||||||
|
SetupPublicRoutes(router)
|
||||||
|
|
||||||
|
// 设置管理端路由
|
||||||
|
SetupAdminRoutes(router)
|
||||||
|
|
||||||
|
// 设置用户端路由
|
||||||
|
SetupUserRoutes(router)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/handlers"
|
||||||
|
"awesomeProject/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupUserRoutes 设置用户端路由
|
||||||
|
func SetupUserRoutes(router *gin.Engine) {
|
||||||
|
// 用户端路由组
|
||||||
|
userGroup := router.Group("/back/user")
|
||||||
|
|
||||||
|
// 应用JWT中间件(用户端需要登录)
|
||||||
|
userGroup.Use(middleware.JWTMiddleware)
|
||||||
|
userGroup.Use(middleware.UserRoleMiddleware())
|
||||||
|
// 创建handler实例
|
||||||
|
productHandler := &handlers.UserProductHandler{}
|
||||||
|
paymentHandler := &handlers.UserPaymentHandler{}
|
||||||
|
addressHandler := &handlers.UserAddressHandler{}
|
||||||
|
orderHandler := &handlers.UserOrderHandler{}
|
||||||
|
warehouseHandler := &handlers.UserWarehouseHandler{}
|
||||||
|
scoreHandler := &handlers.UserScoreHandler{}
|
||||||
|
orderMessageHandler := &handlers.UserOrderMessageHandler{}
|
||||||
|
reconciliationHandler := &handlers.UserReconciliationHandler{}
|
||||||
|
bannerHandler := &handlers.UserBannerHandler{}
|
||||||
|
|
||||||
|
// 首页数据路由
|
||||||
|
userGroup.GET("/home/data", productHandler.GetHomeData) // 获取首页数据(分类和一级商品)
|
||||||
|
|
||||||
|
// 轮播图相关路由
|
||||||
|
userGroup.GET("/banners", bannerHandler.GetActiveBanners) // 获取启用的轮播图列表
|
||||||
|
|
||||||
|
// 商品相关路由
|
||||||
|
productRoutes := userGroup.Group("/products")
|
||||||
|
{
|
||||||
|
// 商品分类
|
||||||
|
productRoutes.GET("/categories", productHandler.GetProductCategories) // 获取商品分类列表
|
||||||
|
|
||||||
|
// 一级商品(官方商品)
|
||||||
|
primaryGroup := productRoutes.Group("/primary")
|
||||||
|
{
|
||||||
|
primaryGroup.GET("", productHandler.GetPrimaryProducts) // 获取一级商品列表
|
||||||
|
primaryGroup.GET("/:id", productHandler.GetPrimaryProduct) // 获取一级商品详情
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级商品(用户挂售商品)
|
||||||
|
secondaryGroup := productRoutes.Group("/secondary")
|
||||||
|
{
|
||||||
|
secondaryGroup.GET("", productHandler.GetSecondaryProducts) // 获取二级商品列表
|
||||||
|
secondaryGroup.GET("/:id", productHandler.GetSecondaryProduct) // 获取二级商品详情
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收款方式相关路由
|
||||||
|
paymentRoutes := userGroup.Group("/payment")
|
||||||
|
{
|
||||||
|
paymentRoutes.GET("/info", paymentHandler.GetPaymentInfo) // 获取收款方式信息
|
||||||
|
paymentRoutes.PUT("/info", paymentHandler.UpdatePaymentInfo) // 更新收款方式信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取卖家收款信息
|
||||||
|
userGroup.GET("/seller/:id/info", paymentHandler.GetSellerInfo) // 获取卖家基本信息
|
||||||
|
userGroup.GET("/seller/:id/payment", paymentHandler.GetSellerPaymentInfo) // 获取卖家收款方式信息
|
||||||
|
|
||||||
|
// 收货地址相关路由
|
||||||
|
addressRoutes := userGroup.Group("/addresses")
|
||||||
|
{
|
||||||
|
addressRoutes.GET("", addressHandler.GetAddressList) // 获取收货地址列表
|
||||||
|
addressRoutes.GET("/:id", addressHandler.GetAddress) // 获取单个收货地址
|
||||||
|
addressRoutes.POST("", addressHandler.CreateAddress) // 创建收货地址
|
||||||
|
addressRoutes.PUT("/:id", addressHandler.UpdateAddress) // 更新收货地址
|
||||||
|
addressRoutes.DELETE("/:id", addressHandler.DeleteAddress) // 删除收货地址
|
||||||
|
addressRoutes.PUT("/:id/default", addressHandler.SetDefaultAddress) // 设置默认收货地址
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单相关路由
|
||||||
|
orderRoutes := userGroup.Group("/orders")
|
||||||
|
{
|
||||||
|
// 买单相关
|
||||||
|
orderRoutes.POST("/purchase", orderHandler.CreatePurchaseOrder) // 创建买单
|
||||||
|
orderRoutes.GET("/purchase", orderHandler.GetPurchaseOrders) // 获取买单列表
|
||||||
|
orderRoutes.GET("/purchase/:id", orderHandler.GetPurchaseOrderDetail) // 获取买单详情
|
||||||
|
orderRoutes.PUT("/purchase/:id/confirm", orderHandler.ConfirmPurchaseOrder) // 确认买单(买家确认收货)
|
||||||
|
orderRoutes.PUT("/purchase/:id/payment", orderHandler.UploadPaymentProof) // 上传付款凭证
|
||||||
|
orderRoutes.PUT("/purchase/:id/cancel", orderHandler.CancelOrder) // 取消订单
|
||||||
|
|
||||||
|
// 卖单相关
|
||||||
|
orderRoutes.GET("/sales", orderHandler.GetSalesOrders) // 获取卖单列表
|
||||||
|
orderRoutes.GET("/sales/:id", orderHandler.GetSalesOrderDetail) // 获取卖单详情
|
||||||
|
orderRoutes.PUT("/sales/:id/confirm", orderHandler.ConfirmSalesOrder) // 确认卖单(卖家确认发货)
|
||||||
|
|
||||||
|
// 订单留言相关
|
||||||
|
orderRoutes.GET("/:order_id/messages", orderMessageHandler.GetOrderMessages) // 获取订单留言列表
|
||||||
|
orderRoutes.POST("/:order_id/messages", orderMessageHandler.CreateOrderMessage) // 创建订单留言
|
||||||
|
|
||||||
|
// 订单统计
|
||||||
|
orderRoutes.GET("/stats", orderHandler.GetOrderStats) // 获取订单统计信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仓库相关路由
|
||||||
|
warehouseRoutes := userGroup.Group("/warehouse")
|
||||||
|
{
|
||||||
|
warehouseRoutes.GET("", warehouseHandler.GetWarehouseList) // 获取仓库商品列表
|
||||||
|
warehouseRoutes.GET("/:id", warehouseHandler.GetWarehouseItem) // 获取单个仓库商品详情
|
||||||
|
warehouseRoutes.POST("/:id/sell", warehouseHandler.SellWarehouseItem) // 出售仓库商品
|
||||||
|
warehouseRoutes.GET("/stats", warehouseHandler.GetWarehouseStats) // 获取仓库统计信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 积分相关路由
|
||||||
|
scoreRoutes := userGroup.Group("/scores")
|
||||||
|
{
|
||||||
|
scoreRoutes.GET("/records", scoreHandler.GetMyScoreRecords) // 获取我的积分记录列表
|
||||||
|
scoreRoutes.GET("/stats", scoreHandler.GetMyScoreStats) // 获取我的积分统计信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对账管理相关路由
|
||||||
|
reconciliationRoutes := userGroup.Group("/reconciliation")
|
||||||
|
{
|
||||||
|
reconciliationRoutes.GET("/stats", reconciliationHandler.GetReconciliationStats) // 获取对账统计数据
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SMSCodeInfo 短信验证码信息
|
||||||
|
type SMSCodeInfo struct {
|
||||||
|
Code string // 验证码
|
||||||
|
Phone string // 手机号
|
||||||
|
IP string // IP地址
|
||||||
|
CreatedAt time.Time // 创建时间
|
||||||
|
ExpiresAt time.Time // 过期时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitInfo 频率限制信息
|
||||||
|
type RateLimitInfo struct {
|
||||||
|
Count int // 请求次数
|
||||||
|
FirstTime time.Time // 首次请求时间
|
||||||
|
LastTime time.Time // 最后请求时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMSService 短信验证码服务
|
||||||
|
type SMSService struct {
|
||||||
|
// 验证码存储:key为手机号,value为验证码信息
|
||||||
|
codes sync.Map // map[string]*SMSCodeInfo
|
||||||
|
|
||||||
|
// 手机号频率限制:key为手机号,value为限制信息
|
||||||
|
phoneLimits sync.Map // map[string]*RateLimitInfo
|
||||||
|
|
||||||
|
// IP频率限制:key为IP地址,value为限制信息
|
||||||
|
ipLimits sync.Map // map[string]*RateLimitInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
smsServiceInstance *SMSService
|
||||||
|
smsServiceOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSMSService 获取短信服务单例
|
||||||
|
func GetSMSService() *SMSService {
|
||||||
|
smsServiceOnce.Do(func() {
|
||||||
|
smsServiceInstance = &SMSService{}
|
||||||
|
// 启动清理协程,定期清理过期数据
|
||||||
|
go smsServiceInstance.cleanup()
|
||||||
|
})
|
||||||
|
return smsServiceInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCode 生成6位数字验证码
|
||||||
|
func (s *SMSService) GenerateCode() string {
|
||||||
|
// 使用时间戳作为随机种子,确保每次生成的验证码都不同
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
return fmt.Sprintf("%06d", r.Intn(1000000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSMS 发送短信验证码
|
||||||
|
// phone: 手机号
|
||||||
|
// ip: 客户端IP地址
|
||||||
|
// 返回:验证码、错误信息
|
||||||
|
func (s *SMSService) SendSMS(phone, ip string) (string, error) {
|
||||||
|
// 1. 验证手机号格式
|
||||||
|
if !s.validatePhone(phone) {
|
||||||
|
return "", fmt.Errorf("手机号格式不正确")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查手机号频率限制(同一手机号1分钟内只能发送1次)
|
||||||
|
if err := s.checkPhoneLimit(phone); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查IP频率限制(同一IP1分钟内最多发送5次)
|
||||||
|
if err := s.checkIPLimit(ip); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 生成验证码
|
||||||
|
code := s.GenerateCode()
|
||||||
|
|
||||||
|
// 5. 存储验证码信息(有效期5分钟)
|
||||||
|
codeInfo := &SMSCodeInfo{
|
||||||
|
Code: code,
|
||||||
|
Phone: phone,
|
||||||
|
IP: ip,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||||
|
}
|
||||||
|
s.codes.Store(phone, codeInfo)
|
||||||
|
|
||||||
|
// 6. 更新频率限制记录
|
||||||
|
s.updatePhoneLimit(phone)
|
||||||
|
s.updateIPLimit(ip)
|
||||||
|
|
||||||
|
// TODO: 这里应该调用真实的短信服务API发送验证码
|
||||||
|
// 目前只返回验证码,实际生产环境需要调用短信服务商API
|
||||||
|
fmt.Printf("[SMS] 发送验证码到 %s: %s (IP: %s)\n", phone, code, ip)
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCode 验证验证码
|
||||||
|
func (s *SMSService) VerifyCode(phone, code string) bool {
|
||||||
|
value, ok := s.codes.Load(phone)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
codeInfo := value.(*SMSCodeInfo)
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if time.Now().After(codeInfo.ExpiresAt) {
|
||||||
|
s.codes.Delete(phone)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码匹配
|
||||||
|
if codeInfo.Code == code {
|
||||||
|
// 验证成功后删除验证码(一次性使用)
|
||||||
|
s.codes.Delete(phone)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePhone 验证手机号格式
|
||||||
|
func (s *SMSService) validatePhone(phone string) bool {
|
||||||
|
if len(phone) != 11 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 简单验证:1开头,第二位3-9
|
||||||
|
if phone[0] != '1' || phone[1] < '3' || phone[1] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPhoneLimit 检查手机号频率限制
|
||||||
|
// 同一手机号1分钟内只能发送1次
|
||||||
|
func (s *SMSService) checkPhoneLimit(phone string) error {
|
||||||
|
value, ok := s.phoneLimits.Load(phone)
|
||||||
|
if !ok {
|
||||||
|
return nil // 没有限制记录,可以发送
|
||||||
|
}
|
||||||
|
|
||||||
|
limitInfo := value.(*RateLimitInfo)
|
||||||
|
// 检查是否在限制时间内(1分钟)
|
||||||
|
if time.Since(limitInfo.LastTime) < 1*time.Minute {
|
||||||
|
return fmt.Errorf("操作过于频繁,请1分钟后再试")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkIPLimit 检查IP频率限制
|
||||||
|
// 同一IP1分钟内最多发送5次
|
||||||
|
func (s *SMSService) checkIPLimit(ip string) error {
|
||||||
|
value, ok := s.ipLimits.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return nil // 没有限制记录,可以发送
|
||||||
|
}
|
||||||
|
|
||||||
|
limitInfo := value.(*RateLimitInfo)
|
||||||
|
|
||||||
|
// 如果超过1分钟,重置计数
|
||||||
|
if time.Since(limitInfo.FirstTime) >= 1*time.Minute {
|
||||||
|
limitInfo.Count = 0
|
||||||
|
limitInfo.FirstTime = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超过限制(1分钟内最多5次)
|
||||||
|
if limitInfo.Count >= 5 {
|
||||||
|
return fmt.Errorf("请求过于频繁,请稍后再试")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updatePhoneLimit 更新手机号限制记录
|
||||||
|
func (s *SMSService) updatePhoneLimit(phone string) {
|
||||||
|
value, ok := s.phoneLimits.Load(phone)
|
||||||
|
if !ok {
|
||||||
|
// 创建新记录
|
||||||
|
s.phoneLimits.Store(phone, &RateLimitInfo{
|
||||||
|
Count: 1,
|
||||||
|
FirstTime: time.Now(),
|
||||||
|
LastTime: time.Now(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limitInfo := value.(*RateLimitInfo)
|
||||||
|
limitInfo.Count++
|
||||||
|
limitInfo.LastTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateIPLimit 更新IP限制记录
|
||||||
|
func (s *SMSService) updateIPLimit(ip string) {
|
||||||
|
value, ok := s.ipLimits.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
// 创建新记录
|
||||||
|
s.ipLimits.Store(ip, &RateLimitInfo{
|
||||||
|
Count: 1,
|
||||||
|
FirstTime: time.Now(),
|
||||||
|
LastTime: time.Now(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limitInfo := value.(*RateLimitInfo)
|
||||||
|
|
||||||
|
// 如果超过1分钟,重置计数
|
||||||
|
if time.Since(limitInfo.FirstTime) >= 1*time.Minute {
|
||||||
|
limitInfo.Count = 1
|
||||||
|
limitInfo.FirstTime = time.Now()
|
||||||
|
limitInfo.LastTime = time.Now()
|
||||||
|
} else {
|
||||||
|
limitInfo.Count++
|
||||||
|
limitInfo.LastTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup 定期清理过期数据
|
||||||
|
func (s *SMSService) cleanup() {
|
||||||
|
ticker := time.NewTicker(2 * time.Minute) // 每10分钟清理一次
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 清理过期的验证码
|
||||||
|
s.codes.Range(func(key, value interface{}) bool {
|
||||||
|
codeInfo := value.(*SMSCodeInfo)
|
||||||
|
if now.After(codeInfo.ExpiresAt) {
|
||||||
|
s.codes.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理过期的频率限制记录(超过1小时未使用的记录)
|
||||||
|
s.phoneLimits.Range(func(key, value interface{}) bool {
|
||||||
|
limitInfo := value.(*RateLimitInfo)
|
||||||
|
if now.Sub(limitInfo.LastTime) > 1*time.Hour {
|
||||||
|
s.phoneLimits.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
s.ipLimits.Range(func(key, value interface{}) bool {
|
||||||
|
limitInfo := value.(*RateLimitInfo)
|
||||||
|
if now.Sub(limitInfo.LastTime) > 1*time.Hour {
|
||||||
|
s.ipLimits.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AutoCancelUnpaidOrders 自动取消超过30分钟未付款的订单
|
||||||
|
func AutoCancelUnpaidOrders() {
|
||||||
|
log.Println("开始执行自动取消未付款订单任务...")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
var cancelOrders []models.PurchaseOrder
|
||||||
|
if err := db.Model(&models.PurchaseOrder{}).Where("order_status = ? and product_type=?", 3, 2).Find(&cancelOrders).Error; err != nil {
|
||||||
|
log.Printf("查询未付款订单失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var Ids []uint
|
||||||
|
for _, order := range cancelOrders {
|
||||||
|
Ids = append(Ids, order.ID)
|
||||||
|
}
|
||||||
|
fmt.Println(cancelOrders)
|
||||||
|
db.Model(&models.SalesOrder{}).Where("purchase_order_id in ?", Ids).Update("order_status", 3)
|
||||||
|
|
||||||
|
var config models.SystemConfig
|
||||||
|
db.Where("config_key=?", "order_over_time").First(&config)
|
||||||
|
value, _ := strconv.Atoi(config.ConfigValue)
|
||||||
|
thirtyMinutesAgo := time.Now().Add(-time.Duration(value) * time.Minute)
|
||||||
|
// 查找所有超过30分钟未付款的订单
|
||||||
|
var unpaidOrders []models.PurchaseOrder
|
||||||
|
if err := db.Where("order_status = ? AND created_at < ?", 0, thirtyMinutesAgo).Find(&unpaidOrders).Error; err != nil {
|
||||||
|
log.Printf("查询未付款订单失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("找到 %d 个需要自动取消的未付款订单", len(unpaidOrders))
|
||||||
|
|
||||||
|
// 处理每个未付款订单
|
||||||
|
for _, order := range unpaidOrders {
|
||||||
|
// 开启事务
|
||||||
|
tx := db.Begin()
|
||||||
|
|
||||||
|
// 更新订单状态为已取消
|
||||||
|
if err := tx.Model(&order).Update("order_status", 3).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("取消订单 %d 失败: %v", order.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复商品库存
|
||||||
|
if order.ProductType == 1 { // 一级商品
|
||||||
|
if err := tx.Model(&models.PrimaryProduct{}).Where("id = ?", order.ProductID).
|
||||||
|
Update("stock_quantity", gorm.Expr("stock_quantity + ?", order.Quantity)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("恢复一级商品 %d 库存失败: %v", order.ProductID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else { // 二级商品
|
||||||
|
if err := tx.Model(&models.SecondaryProduct{}).Where("id = ?", order.ProductID).
|
||||||
|
Update("quantity", gorm.Expr("quantity + ?", order.Quantity)).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("恢复二级商品 %d 库存失败: %v", order.ProductID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是二级商品,还需要取消对应的卖家订单
|
||||||
|
var salesOrder models.SalesOrder
|
||||||
|
if err := tx.Where("purchase_order_id = ?", order.ID).First(&salesOrder).Error; err != nil {
|
||||||
|
if err != gorm.ErrRecordNotFound {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("查询订单 %d 对应的卖家订单失败: %v", order.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 更新卖家订单状态为已取消
|
||||||
|
if err := tx.Model(&salesOrder).Update("order_status", 3).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("取消卖家订单 %d 失败: %v", salesOrder.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
log.Printf("提交事务失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("成功自动取消订单 ID: %d, 订单号: %s", order.ID, order.OrderNo)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("自动取消未付款订单任务执行完成")
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"awesomeProject/internal/models"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckUserExpiry 检查用户过期状态并禁用过期用户
|
||||||
|
func CheckUserExpiry() {
|
||||||
|
log.Println("开始执行用户过期检查任务...")
|
||||||
|
|
||||||
|
db := common.GetDB()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 查找所有已过期但仍然启用的用户
|
||||||
|
var expiredUsers []models.User
|
||||||
|
if err := db.Where("expiry_date IS NOT NULL AND expiry_date < ? AND status = ?", now, 1).Find(&expiredUsers).Error; err != nil {
|
||||||
|
log.Printf("查询过期用户失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expiredUsers) == 0 {
|
||||||
|
log.Println("没有发现过期用户")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("发现 %d 个过期用户需要禁用", len(expiredUsers))
|
||||||
|
|
||||||
|
// 批量禁用过期用户
|
||||||
|
var userIDs []uint
|
||||||
|
for _, user := range expiredUsers {
|
||||||
|
userIDs = append(userIDs, user.ID)
|
||||||
|
log.Printf("用户 ID: %d, 手机号: %s, 过期时间: %v",
|
||||||
|
user.ID, user.Phone, user.ExpiryDate.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用事务批量更新状态
|
||||||
|
tx := db.Begin()
|
||||||
|
if err := tx.Model(&models.User{}).Where("id IN ?", userIDs).Update("status", 0).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("批量禁用过期用户失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
log.Printf("提交事务失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("成功禁用 %d 个过期用户", len(userIDs))
|
||||||
|
log.Println("用户过期检查任务执行完成")
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CronTask *cron.Cron
|
||||||
|
|
||||||
|
// InitCronTasks 初始化所有定时任务
|
||||||
|
func InitCronTasks() {
|
||||||
|
log.Println("初始化定时任务...")
|
||||||
|
CronTask = cron.New()
|
||||||
|
|
||||||
|
// 添加定时任务:每15分钟检查一次未付款订单
|
||||||
|
_, err := CronTask.AddFunc("*/15 * * * *", AutoCancelUnpaidOrders)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("添加自动取消订单任务失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("已添加自动取消订单任务,每15分钟执行一次")
|
||||||
|
}
|
||||||
|
go AutoCancelUnpaidOrders()
|
||||||
|
|
||||||
|
// 添加定时任务:每小时检查一次用户过期状态
|
||||||
|
_, err = CronTask.AddFunc("0 * * * *", CheckUserExpiry)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("添加用户过期检查任务失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("已添加用户过期检查任务,每小时执行一次")
|
||||||
|
}
|
||||||
|
// 立即执行一次用户过期检查
|
||||||
|
go CheckUserExpiry()
|
||||||
|
// 启动所有定时任务
|
||||||
|
CronTask.Start()
|
||||||
|
log.Println("所有定时任务已启动")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加单个定时任务(保留原有函数以兼容旧代码)
|
||||||
|
func addTask(cronn string, work func()) {
|
||||||
|
_, err := CronTask.AddFunc(cronn, work)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("添加定时任务失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package jwtutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"awesomeProject/internal/common"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getSecretKey 获取密钥
|
||||||
|
func getSecretKey() []byte {
|
||||||
|
if common.MineConfig != nil {
|
||||||
|
return []byte(common.MineConfig.JWT.SecretKey)
|
||||||
|
}
|
||||||
|
// 默认密钥(仅开发环境使用)
|
||||||
|
return []byte("your-very-secret-key-32-bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenExpireDuration 获取Token过期时间
|
||||||
|
func getTokenExpireDuration() time.Duration {
|
||||||
|
if common.MineConfig != nil {
|
||||||
|
return time.Duration(common.MineConfig.JWT.ExpireHours) * time.Hour
|
||||||
|
}
|
||||||
|
// 默认24小时
|
||||||
|
return time.Hour * 24
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用:生成 Token,支持任意结构体
|
||||||
|
func GenerateToken(claims interface{}) (string, error) {
|
||||||
|
// 1. 将结构体转换为 map[string]interface{}
|
||||||
|
claimMap, err := structToMap(claims)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 添加标准声明(exp, iat, nbf)
|
||||||
|
claimMap["exp"] = time.Now().Add(getTokenExpireDuration()).Unix()
|
||||||
|
claimMap["iat"] = time.Now().Unix()
|
||||||
|
claimMap["nbf"] = time.Now().Unix()
|
||||||
|
|
||||||
|
// 3. 创建 Token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(claimMap))
|
||||||
|
|
||||||
|
// 4. 签名
|
||||||
|
return token.SignedString(getSecretKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTokenToMap(tokenString string) (map[string]interface{}, error) {
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return getSecretKey(), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, errors.New("token is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("token claims is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseToken(tokenString string, dest interface{}) error {
|
||||||
|
|
||||||
|
if reflect.TypeOf(dest).Kind() != reflect.Ptr {
|
||||||
|
return errors.New("dest must be a pointer to a struct")
|
||||||
|
}
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return getSecretKey(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return errors.New("token 无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 MapClaims
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||||
|
// 将 MapClaims 转回结构体
|
||||||
|
if err := mapToStruct(claims, dest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("无法解析 token 声明")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将结构体转为 map[string]interface{}
|
||||||
|
func structToMap(obj interface{}) (map[string]interface{}, error) {
|
||||||
|
val := reflect.ValueOf(obj)
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
val = val.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.Kind() != reflect.Struct {
|
||||||
|
return nil, errors.New("输入必须是结构体")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
typ := val.Type()
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := val.Field(i)
|
||||||
|
fieldType := typ.Field(i)
|
||||||
|
jsonTag := fieldType.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := jsonTag
|
||||||
|
result[key] = field.Interface()
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 map[string]interface{} 转回结构体
|
||||||
|
func mapToStruct(data map[string]interface{}, dest interface{}) error {
|
||||||
|
val := reflect.ValueOf(dest)
|
||||||
|
if val.Kind() != reflect.Ptr {
|
||||||
|
return errors.New("dest 必须是指针")
|
||||||
|
}
|
||||||
|
val = val.Elem()
|
||||||
|
if val.Kind() != reflect.Struct {
|
||||||
|
return errors.New("dest 必须是指针指向结构体")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := val.Field(i)
|
||||||
|
fieldType := val.Type().Field(i)
|
||||||
|
jsonTag := fieldType.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.CanSet() {
|
||||||
|
value, ok := data[jsonTag]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动类型转换
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
field.SetString(s)
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int64:
|
||||||
|
if n, ok := value.(float64); ok {
|
||||||
|
field.SetInt(int64(n))
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint64:
|
||||||
|
if n, ok := value.(float64); ok {
|
||||||
|
field.SetUint(uint64(n))
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
if b, ok := value.(bool); ok {
|
||||||
|
field.SetBool(b)
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
if f, ok := value.(float64); ok {
|
||||||
|
field.SetFloat(f)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func ValidateToken(tokenString string, dest interface{}) bool {
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return getSecretKey(), nil
|
||||||
|
})
|
||||||
|
return err == nil && token.Valid && ParseToken(tokenString, dest) == nil
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user