For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在 Phase 0 角色模型基础上,搭建完整的商品发布、浏览、购买、订单管理、系统配置能力,为 Phase 2 会员体系提供商品基础。
Architecture: 3 个新实体(Product/ProductOrder/SysConfig)+ 3 个新 Controller 包(product/ + config/)+ 5 个新小程序页面 + 3 个新 Web 管理页面。服务商可发布商品(需审核),用户可在发现/商城浏览并下单,管理员可在 Web 端审核/管理商品和订单。商品价格支持原价+会员价,等值商品选择留给 Phase 2。
Tech Stack: Java 1.8 + Spring Boot 2.7.18 + MyBatis-Plus + uni-app Vue 2 + Element UI
Spec reference: docs/superpowers/specs/2026-06-02-care-family-club-design.md (溪福俱乐部 §3.2-3.6, §9.2, §10.1, §11.1)
依赖关系:
Key constraints:
java.util.Date 和 implements Serializable@RequestMapping("/api/...") + @PostMapping@Resource?.| 任务组 | 范围 | 任务数 |
|---|---|---|
| T1: 数据库初始化 | Product/Order/SysConfig 3 张表 DDL + 商品种子数据 | 1 |
| T2: Product 实体+Mapper+Service | 商品 CRUD + 列表查询 | 4 |
| T3: ProductOrder 实体+Mapper+Service | 订单创建/支付/状态流转 | 3 |
| T4: SysConfig 实体+Mapper+Service | 系统配置 CRUD | 2 |
| T5: 后端 API Controller | ProductController / ProductOrderController / SysConfigController / ProductVendorController | 4 |
| T6: 前端 API 封装 | cfc-frontend/utils/api.js 新增商品/订单/配置 API | 1 |
| T7: 商城页迭代 | shop/index.vue 从占位改为商品列表(含筛选) | 1 |
| T8: 发现页迭代 | discover/index.vue 从占位改为活动+课程列表 | 1 |
| T9: 商品详情页 | discover/product-detail.vue(可复用商城/发现) | 1 |
| T10: 订单列表/详情页 | shop/order-list.vue + shop/order-detail.vue | 2 |
| T11: 服务商中心扩展 | vendor/center.vue 添加商品管理/订单管理入口 + 新增 vendor/products.vue + vendor/orders.vue | 3 |
| T12: Web 商品管理 | admin/ProductManage.vue + api/product.js + router + Layout | 4 |
| T13: Web 订单管理 | admin/OrderManage.vue | 1 |
| T14: Web 系统配置 | admin/SysConfig.vue | 1 |
| T15: 验证 | 后端编译 + 前端 lint + 端到端测试 | 1 |
| 总计 | 30 任务 |
Files:
Modify: cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
[ ] Step 1: 在 DatabaseInitializer 末尾("数据库迁移完成" 日志前)追加 3 张表 DDL
找到 runMigrations() 方法结束位置("数据库迁移完成" log 之前),添加:
// ==================== Phase 1: 溪福俱乐部商品系统 ====================
// 商品表
jdbcTemplate.execute(
"CREATE TABLE IF NOT EXISTS products (" +
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
" name VARCHAR(200) NOT NULL," +
" description VARCHAR(1000)," +
" intro TEXT," +
" cover_image VARCHAR(500)," +
" images TEXT," +
" price DECIMAL(10,2) NOT NULL DEFAULT 0," +
" stock INT NOT NULL DEFAULT 0," +
" sales_count INT NOT NULL DEFAULT 0," +
" product_type VARCHAR(32) NOT NULL COMMENT 'activity/course/physical/digital/service'," +
" vendor_id BIGINT," +
" vendor_name VARCHAR(100)," +
" status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/pending/approved/rejected/on_shelf/off_shelf'," +
" reject_reason VARCHAR(500)," +
" member_price DECIMAL(10,2)," +
" member_eligible VARCHAR(32) DEFAULT 'none' COMMENT 'all/budget/none'," +
" domain VARCHAR(32) COMMENT 'action/mind/body/wisdom/wealth'," +
" external_source VARCHAR(64)," +
" external_id VARCHAR(128)," +
" external_data TEXT," +
" created_at DATETIME," +
" updated_at DATETIME" +
")"
);
// 订单表
jdbcTemplate.execute(
"CREATE TABLE IF NOT EXISTS product_orders (" +
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
" order_no VARCHAR(64) NOT NULL," +
" product_id BIGINT NOT NULL," +
" product_name VARCHAR(200)," +
" product_type VARCHAR(32)," +
" buyer_id BIGINT NOT NULL," +
" family_id BIGINT," +
" quantity INT NOT NULL DEFAULT 1," +
" unit_price DECIMAL(10,2) NOT NULL," +
" total_amount DECIMAL(10,2) NOT NULL," +
" status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/completed/cancelled/refunded'," +
" payment_method VARCHAR(32) COMMENT 'wechat/points/membership'," +
" remark VARCHAR(500)," +
" paid_at DATETIME," +
" created_at DATETIME," +
" updated_at DATETIME" +
")"
);
// 系统配置表
jdbcTemplate.execute(
"CREATE TABLE IF NOT EXISTS sys_config (" +
" id BIGINT AUTO_INCREMENT PRIMARY KEY," +
" config_key VARCHAR(64) NOT NULL," +
" config_value TEXT," +
" description VARCHAR(500)," +
" updated_at DATETIME" +
")"
);
// SysConfig 种子数据
insertSysConfigSeed("points_exchange_rate", "0.01", "积分兑换汇率(1积分=0.01元)");
insertSysConfigSeed("points_deduct_cap", "0.5", "积分抵扣上限(50%)");
insertSysConfigSeed("member_fee_standard", "1314", "标准会员年费(元)");
insertSysConfigSeed("member_fee_premium", "3650", "高级会员年费(元)");
insertSysConfigSeed("member_eligible_budget", "1314", "等值商品选择预算上限(元)");
insertSysConfigSeed("renewal_discount_rate", "0.85", "续费折扣率");
insertSysConfigSeed("member_discount_standard", "0.9", "标准会员折扣率");
insertSysConfigSeed("member_discount_premium", "0.8", "高级会员折扣率");
// Product 种子数据(5 个示例商品,覆盖全部 5 种 productType)
// 注意:vendor_id 设为 NULL(平台官方商品,无服务商归属)
insertProductSeed("亲子关系工作坊", "线下半日工作坊,重建家庭对话模式", "activity", 388.00, 1L, "action", "all");
insertProductSeed("情绪管理21天训练营", "21天视频课程,识别并管理情绪", "course", 199.00, 1L, "mind", "all");
insertProductSeed("肠道菌群检测套装", "家用检测 + 报告解读", "physical", 599.00, 1L, "body", "budget");
insertProductSeed("儿童多模态认知测评", "在线测评 + 个性化建议", "digital", 99.00, 1L, "wisdom", "none");
insertProductSeed("成长规划师1对1咨询", "60分钟专业规划", "service", 500.00, 1L, "wealth", "budget");
在类的末尾(最后一个方法之后)添加:
private void insertSysConfigSeed(String key, String value, String desc) {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM sys_config WHERE config_key = ?", Integer.class, key);
if (count == null || count == 0) {
jdbcTemplate.update(
"INSERT INTO sys_config (config_key, config_value, description, updated_at) VALUES (?, ?, ?, NOW())",
key, value, desc);
}
}
private void insertProductSeed(String name, String desc, String type,
java.math.BigDecimal price, Long vendorId,
String domain, String memberEligible) {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM products WHERE name = ? AND product_type = ?", Integer.class, name, type);
if (count == null || count == 0) {
jdbcTemplate.update(
"INSERT INTO products (name, description, price, product_type, vendor_id, vendor_name, status, domain, member_eligible, created_at, updated_at) " +
"VALUES (?, ?, ?, ?, ?, '平台官方', 'on_shelf', ?, ?, NOW(), NOW())",
name, desc, price, type, vendorId, domain, memberEligible);
}
}
[ ] Step 3: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok(无错误)
[ ] Step 4: 启动后端验证表创建成功
# 查找现有后端进程
ps aux | grep "cfc-backend\|spring-boot" | grep -v grep
# 如果进程存在,kill 后重启
pkill -f "cfc-backend" 2>/dev/null || true
sleep 2
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn spring-boot:run &
sleep 30 # 等待启动
# 检查表是否创建(用 mysql 客户端或 REST)
curl -s -X POST http://localhost:8080/api/product/list -H "Content-Type: application/json" -d '{"page":1,"size":10}'
Expected: 返回 JSON 含 code: 200,5 个商品数据(虽然 ProductController 还没实现,会报 404,这是正常的)
[ ] Step 5: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
git commit -m "feat(phase1): 商品/订单/系统配置 3 张表 DDL + 种子数据"
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java
[ ] Step 1: 创建 Product.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("products")
public class Product implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private String intro;
private String coverImage;
private String images;
private BigDecimal price;
private Integer stock;
private Integer salesCount;
private String productType; // activity/course/physical/digital/service
private Long vendorId;
private String vendorName;
private String status; // draft/pending/approved/rejected/on_shelf/off_shelf
private String rejectReason;
private BigDecimal memberPrice;
private String memberEligible; // all/budget/none
private String domain; // action/mind/body/wisdom/wealth
private String externalSource;
private String externalId;
private String externalData;
private Date createdAt;
private Date updatedAt;
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductMapper.java
[ ] Step 1: 创建 ProductMapper.java
package com.etotem.cfc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.etotem.cfc.entity.Product;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/dto/ProductListQueryDTO.java
[ ] Step 1: 创建 ProductDTO.java
package com.etotem.cfc.dto;
import com.etotem.cfc.entity.Product;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class ProductDTO {
private Long id;
private String name;
private String description;
private String intro;
private String coverImage;
private String images;
private BigDecimal price;
private BigDecimal memberPrice;
private Integer stock;
private Integer salesCount;
private String productType;
private Long vendorId;
private String vendorName;
private String status;
private String domain;
private String memberEligible;
private Date createdAt;
private Date updatedAt;
public static ProductDTO from(Product p) {
if (p == null) return null;
ProductDTO d = new ProductDTO();
d.id = p.getId();
d.name = p.getName();
d.description = p.getDescription();
d.intro = p.getIntro();
d.coverImage = p.getCoverImage();
d.images = p.getImages();
d.price = p.getPrice();
d.memberPrice = p.getMemberPrice();
d.stock = p.getStock();
d.salesCount = p.getSalesCount();
d.productType = p.getProductType();
d.vendorId = p.getVendorId();
d.vendorName = p.getVendorName();
d.status = p.getStatus();
d.domain = p.getDomain();
d.memberEligible = p.getMemberEligible();
d.createdAt = p.getCreatedAt();
d.updatedAt = p.getUpdatedAt();
return d;
}
}
[ ] Step 2: 创建 ProductListQueryDTO.java
package com.etotem.cfc.dto;
import lombok.Data;
@Data
public class ProductListQueryDTO {
private Integer page = 1;
private Integer size = 10;
private String productType; // 可选筛选
private String domain; // 可选筛选
private Long vendorId; // 可选筛选(查询某服务商的商品)
private String keyword; // 可选搜索关键字
}
[ ] Step 3: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
[ ] Step 1: 创建 ProductService.java
package com.etotem.cfc.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.ProductDTO;
import com.etotem.cfc.dto.ProductListQueryDTO;
import com.etotem.cfc.entity.Product;
import com.etotem.cfc.entity.User;
import com.etotem.cfc.mapper.ProductMapper;
import com.etotem.cfc.mapper.UserMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class ProductService {
@Resource
private ProductMapper productMapper;
@Resource
private UserMapper userMapper;
/**
* 商品列表(公开 - 仅显示已上架商品)
*/
public Result<Map<String, Object>> list(ProductListQueryDTO query) {
Page<Product> page = new Page<>(query.getPage(), query.getSize());
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
.eq(Product::getStatus, "on_shelf")
.orderByDesc(Product::getCreatedAt);
if (query.getProductType() != null && !query.getProductType().isEmpty()) {
wrapper.eq(Product::getProductType, query.getProductType());
}
if (query.getDomain() != null && !query.getDomain().isEmpty()) {
wrapper.eq(Product::getDomain, query.getDomain());
}
if (query.getKeyword() != null && !query.getKeyword().isEmpty()) {
wrapper.like(Product::getName, query.getKeyword());
}
IPage<Product> result = productMapper.selectPage(page, wrapper);
List<ProductDTO> records = result.getRecords().stream()
.map(ProductDTO::from)
.collect(Collectors.toList());
Map<String, Object> data = new HashMap<>();
data.put("records", records);
data.put("total", result.getTotal());
data.put("page", result.getCurrent());
data.put("size", result.getSize());
return Result.success(data);
}
/**
* 商品详情(公开)
*/
public Result<ProductDTO> detail(Long id) {
Product product = productMapper.selectById(id);
if (product == null) {
return Result.error("商品不存在");
}
if (!"on_shelf".equals(product.getStatus()) && !"approved".equals(product.getStatus())) {
// 非上架商品,仅创建者和管理员可见
return Result.error("商品未上架");
}
return Result.success(ProductDTO.from(product));
}
/**
* 服务商发布商品
*/
public Result<ProductDTO> create(Product product, Long vendorId) {
if (vendorId == null) {
return Result.error("请先登录");
}
User vendor = userMapper.selectById(vendorId);
if (vendor == null || !"approved".equals(vendor.getVendorStatus())) {
return Result.error("仅审核通过的服务商可发布商品");
}
product.setVendorId(vendorId);
product.setVendorName(vendor.getNickname() != null ? vendor.getNickname() : vendor.getRealName());
product.setStatus("pending"); // 待管理员审核
product.setCreatedAt(new Date());
product.setUpdatedAt(new Date());
productMapper.insert(product);
return Result.success(ProductDTO.from(product));
}
/**
* 服务商更新商品
*/
public Result<ProductDTO> update(Product product, Long vendorId) {
Product existing = productMapper.selectById(product.getId());
if (existing == null) {
return Result.error("商品不存在");
}
if (!existing.getVendorId().equals(vendorId)) {
return Result.error("无权修改此商品");
}
// 服务商更新后回到 pending 状态待审核
product.setStatus("pending");
product.setUpdatedAt(new Date());
productMapper.updateById(product);
return Result.success(ProductDTO.from(productMapper.selectById(product.getId())));
}
/**
* 服务商查询自己的商品(含各状态)
*/
public Result<List<ProductDTO>> myProducts(Long vendorId) {
List<Product> list = productMapper.selectList(
new LambdaQueryWrapper<Product>()
.eq(Product::getVendorId, vendorId)
.orderByDesc(Product::getCreatedAt)
);
List<ProductDTO> dtos = list.stream().map(ProductDTO::from).collect(Collectors.toList());
return Result.success(dtos);
}
/**
* 服务商上架/下架商品
*/
public Result<String> shelve(Long productId, Long vendorId, String action) {
Product product = productMapper.selectById(productId);
if (product == null) {
return Result.error("商品不存在");
}
if (!product.getVendorId().equals(vendorId)) {
return Result.error("无权操作");
}
if (!"approved".equals(product.getStatus())) {
return Result.error("商品未通过审核,无法上下架");
}
if ("shelve".equals(action)) {
product.setStatus("on_shelf");
} else if ("unshelve".equals(action)) {
product.setStatus("off_shelf");
} else {
return Result.error("操作类型错误");
}
product.setUpdatedAt(new Date());
productMapper.updateById(product);
return Result.success("操作成功");
}
/**
* 管理员审核商品
*/
public Result<String> review(Long productId, String action, String rejectReason) {
Product product = productMapper.selectById(productId);
if (product == null) {
return Result.error("商品不存在");
}
if (!"pending".equals(product.getStatus())) {
return Result.error("仅待审核商品可审核");
}
if ("approve".equals(action)) {
product.setStatus("approved");
product.setRejectReason(null);
} else if ("reject".equals(action)) {
product.setStatus("rejected");
product.setRejectReason(rejectReason != null ? rejectReason : "不符合规范");
} else {
return Result.error("操作类型错误");
}
product.setUpdatedAt(new Date());
productMapper.updateById(product);
return Result.success("操作成功");
}
/**
* 管理员查询待审核商品
*/
public Result<List<ProductDTO>> pendingReview() {
List<Product> list = productMapper.selectList(
new LambdaQueryWrapper<Product>()
.eq(Product::getStatus, "pending")
.orderByAsc(Product::getCreatedAt)
);
List<ProductDTO> dtos = list.stream().map(ProductDTO::from).collect(Collectors.toList());
return Result.success(dtos);
}
/**
* 管理员查询所有商品
*/
public Result<List<ProductDTO>> adminList(String status) {
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
.orderByDesc(Product::getCreatedAt);
if (status != null && !status.isEmpty()) {
wrapper.eq(Product::getStatus, status);
}
List<Product> list = productMapper.selectList(wrapper);
List<ProductDTO> dtos = list.stream().map(ProductDTO::from).collect(Collectors.toList());
return Result.success(dtos);
}
/**
* 减少库存(订单创建时调用)
*/
public boolean decreaseStock(Long productId, Integer quantity) {
Product product = productMapper.selectById(productId);
if (product == null || product.getStock() < quantity) {
return false;
}
product.setStock(product.getStock() - quantity);
product.setSalesCount((product.getSalesCount() != null ? product.getSalesCount() : 0) + quantity);
product.setUpdatedAt(new Date());
productMapper.updateById(product);
return true;
}
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductMapper.java cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java cfc-backend/src/main/java/com/etotem/cfc/dto/ProductListQueryDTO.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
git commit -m "feat(phase1): Product 实体/Mapper/Service 实现"
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.javacfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderMapper.javacfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java
[ ] Step 1: 创建 ProductOrder.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("product_orders")
public class ProductOrder implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long productId;
private String productName;
private String productType;
private Long buyerId;
private Long familyId;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal totalAmount;
private String status; // pending/paid/completed/cancelled/refunded
private String paymentMethod; // wechat/points/membership
private String remark;
private Date paidAt;
private Date createdAt;
private Date updatedAt;
}
[ ] Step 2: 创建 ProductOrderMapper.java
package com.etotem.cfc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.etotem.cfc.entity.ProductOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductOrderMapper extends BaseMapper<ProductOrder> {
}
[ ] Step 3: 创建 ProductOrderDTO.java
package com.etotem.cfc.dto;
import com.etotem.cfc.entity.ProductOrder;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class ProductOrderDTO {
private Long id;
private String orderNo;
private Long productId;
private String productName;
private String productType;
private Long buyerId;
private String buyerName;
private Long familyId;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal totalAmount;
private String status;
private String paymentMethod;
private String remark;
private Date paidAt;
private Date createdAt;
public static ProductOrderDTO from(ProductOrder o) {
if (o == null) return null;
ProductOrderDTO d = new ProductOrderDTO();
d.id = o.getId();
d.orderNo = o.getOrderNo();
d.productId = o.getProductId();
d.productName = o.getProductName();
d.productType = o.getProductType();
d.buyerId = o.getBuyerId();
d.familyId = o.getFamilyId();
d.quantity = o.getQuantity();
d.unitPrice = o.getUnitPrice();
d.totalAmount = o.getTotalAmount();
d.status = o.getStatus();
d.paymentMethod = o.getPaymentMethod();
d.remark = o.getRemark();
d.paidAt = o.getPaidAt();
d.createdAt = o.getCreatedAt();
return d;
}
}
[ ] Step 4: 创建 CreateProductOrderDTO.java
package com.etotem.cfc.dto;
import lombok.Data;
@Data
public class CreateProductOrderDTO {
private Long productId;
private Integer quantity = 1;
private String paymentMethod; // wechat/points/membership
private String remark;
}
[ ] Step 5: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
[ ] Step 1: 创建 ProductOrderService.java
package com.etotem.cfc.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.CreateProductOrderDTO;
import com.etotem.cfc.dto.ProductOrderDTO;
import com.etotem.cfc.entity.Product;
import com.etotem.cfc.entity.ProductOrder;
import com.etotem.cfc.entity.User;
import com.etotem.cfc.mapper.ProductMapper;
import com.etotem.cfc.mapper.ProductOrderMapper;
import com.etotem.cfc.mapper.UserMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ProductOrderService {
@Resource
private ProductOrderMapper orderMapper;
@Resource
private ProductMapper productMapper;
@Resource
private UserMapper userMapper;
@Resource
private ProductService productService;
/**
* 创建订单
*/
public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
if (buyerId == null) {
return Result.error("请先登录");
}
Product product = productMapper.selectById(dto.getProductId());
if (product == null) {
return Result.error("商品不存在");
}
if (!"on_shelf".equals(product.getStatus())) {
return Result.error("商品已下架");
}
if (product.getStock() != null && product.getStock() < dto.getQuantity()) {
return Result.error("库存不足");
}
User buyer = userMapper.selectById(buyerId);
ProductOrder order = new ProductOrder();
order.setOrderNo(generateOrderNo());
order.setProductId(product.getId());
order.setProductName(product.getName());
order.setProductType(product.getProductType());
order.setBuyerId(buyerId);
order.setFamilyId(buyer != null ? buyer.getFamilyId() : null);
order.setQuantity(dto.getQuantity());
order.setUnitPrice(product.getPrice());
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity())));
order.setStatus("pending");
order.setPaymentMethod(dto.getPaymentMethod() != null ? dto.getPaymentMethod() : "wechat");
order.setRemark(dto.getRemark());
order.setCreatedAt(new Date());
order.setUpdatedAt(new Date());
orderMapper.insert(order);
return Result.success(ProductOrderDTO.from(order));
}
/**
* 支付订单(测试模式:直接标记为已支付)
*/
public Result<ProductOrderDTO> pay(String orderNo, Long userId) {
ProductOrder order = orderMapper.selectOne(
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
if (order == null) {
return Result.error("订单不存在");
}
if (!order.getBuyerId().equals(userId)) {
return Result.error("无权操作此订单");
}
if (!"pending".equals(order.getStatus())) {
return Result.error("订单状态异常");
}
// 减少库存
boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
if (!stockOk) {
return Result.error("库存不足");
}
order.setStatus("paid");
order.setPaidAt(new Date());
order.setUpdatedAt(new Date());
orderMapper.updateById(order);
return Result.success(ProductOrderDTO.from(order));
}
/**
* 取消订单
*/
public Result<String> cancel(String orderNo, Long userId) {
ProductOrder order = orderMapper.selectOne(
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
if (order == null) {
return Result.error("订单不存在");
}
if (!order.getBuyerId().equals(userId)) {
return Result.error("无权操作此订单");
}
if (!"pending".equals(order.getStatus())) {
return Result.error("仅待支付订单可取消");
}
order.setStatus("cancelled");
order.setUpdatedAt(new Date());
orderMapper.updateById(order);
return Result.success("订单已取消");
}
/**
* 买家查询自己的订单
*/
public Result<List<ProductOrderDTO>> myOrders(Long buyerId, String status) {
LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
.eq(ProductOrder::getBuyerId, buyerId)
.orderByDesc(ProductOrder::getCreatedAt);
if (status != null && !status.isEmpty()) {
wrapper.eq(ProductOrder::getStatus, status);
}
List<ProductOrder> list = orderMapper.selectList(wrapper);
return Result.success(list.stream().map(ProductOrderDTO::from).collect(Collectors.toList()));
}
/**
* 订单详情
*/
public Result<ProductOrderDTO> detail(String orderNo, Long userId) {
ProductOrder order = orderMapper.selectOne(
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
if (order == null) {
return Result.error("订单不存在");
}
// 仅买家/卖家/管理员可见
Product product = productMapper.selectById(order.getProductId());
boolean isBuyer = order.getBuyerId().equals(userId);
boolean isVendor = product != null && product.getVendorId() != null && product.getVendorId().equals(userId);
if (!isBuyer && !isVendor) {
return Result.error("无权查看");
}
return Result.success(ProductOrderDTO.from(order));
}
/**
* 服务商查询收到的订单
*/
public Result<List<ProductOrderDTO>> vendorOrders(Long vendorId, String status) {
// 找出该服务商的所有商品 ID
List<Product> products = productMapper.selectList(
new LambdaQueryWrapper<Product>().eq(Product::getVendorId, vendorId));
if (products.isEmpty()) {
return Result.success(java.util.Collections.emptyList());
}
List<Long> productIds = products.stream().map(Product::getId).collect(Collectors.toList());
LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
.in(ProductOrder::getProductId, productIds)
.orderByDesc(ProductOrder::getCreatedAt);
if (status != null && !status.isEmpty()) {
wrapper.eq(ProductOrder::getStatus, status);
}
List<ProductOrder> list = orderMapper.selectList(wrapper);
return Result.success(list.stream().map(ProductOrderDTO::from).collect(Collectors.toList()));
}
/**
* 管理员查询所有订单
*/
public Result<List<ProductOrderDTO>> adminList(String status) {
LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
.orderByDesc(ProductOrder::getCreatedAt);
if (status != null && !status.isEmpty()) {
wrapper.eq(ProductOrder::getStatus, status);
}
List<ProductOrder> list = orderMapper.selectList(wrapper);
return Result.success(list.stream().map(ProductOrderDTO::from).collect(Collectors.toList()));
}
/**
* 服务商确认订单完成
*/
public Result<String> confirm(String orderNo, Long vendorId) {
ProductOrder order = orderMapper.selectOne(
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
if (order == null) {
return Result.error("订单不存在");
}
Product product = productMapper.selectById(order.getProductId());
if (product == null || !product.getVendorId().equals(vendorId)) {
return Result.error("无权操作");
}
if (!"paid".equals(order.getStatus())) {
return Result.error("仅已支付订单可确认完成");
}
order.setStatus("completed");
order.setUpdatedAt(new Date());
orderMapper.updateById(order);
return Result.success("订单已完成");
}
private String generateOrderNo() {
return "PO" + System.currentTimeMillis();
}
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductOrderMapper.java cfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.java cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
git commit -m "feat(phase1): ProductOrder 实体/Mapper/Service 实现"
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/SysConfig.javacfc-backend/src/main/java/com/etotem/cfc/mapper/SysConfigMapper.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/dto/SysConfigDTO.java
[ ] Step 1: 创建 SysConfig.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("sys_config")
public class SysConfig implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String configKey;
private String configValue;
private String description;
private Date updatedAt;
}
[ ] Step 2: 创建 SysConfigMapper.java
package com.etotem.cfc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.etotem.cfc.entity.SysConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysConfigMapper extends BaseMapper<SysConfig> {
}
[ ] Step 3: 创建 SysConfigDTO.java
package com.etotem.cfc.dto;
import com.etotem.cfc.entity.SysConfig;
import lombok.Data;
import java.util.Date;
@Data
public class SysConfigDTO {
private Long id;
private String configKey;
private String configValue;
private String description;
private Date updatedAt;
public static SysConfigDTO from(SysConfig c) {
if (c == null) return null;
SysConfigDTO d = new SysConfigDTO();
d.id = c.getId();
d.configKey = c.getConfigKey();
d.configValue = c.getConfigValue();
d.description = c.getDescription();
d.updatedAt = c.getUpdatedAt();
return d;
}
}
[ ] Step 4: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/service/SysConfigService.java
[ ] Step 1: 创建 SysConfigService.java
package com.etotem.cfc.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.SysConfigDTO;
import com.etotem.cfc.entity.SysConfig;
import com.etotem.cfc.mapper.SysConfigMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class SysConfigService {
@Resource
private SysConfigMapper configMapper;
/**
* 获取所有配置(key-value 形式)
*/
public Result<Map<String, String>> getAllAsMap() {
List<SysConfig> list = configMapper.selectList(null);
Map<String, String> map = new HashMap<>();
for (SysConfig c : list) {
map.put(c.getConfigKey(), c.getConfigValue());
}
return Result.success(map);
}
/**
* 获取所有配置(详细列表)
*/
public Result<List<SysConfigDTO>> listAll() {
List<SysConfig> list = configMapper.selectList(
new LambdaQueryWrapper<SysConfig>().orderByAsc(SysConfig::getConfigKey));
return Result.success(list.stream().map(SysConfigDTO::from).collect(Collectors.toList()));
}
/**
* 获取单个配置
*/
public Result<SysConfigDTO> get(String key) {
SysConfig config = configMapper.selectOne(
new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key));
if (config == null) {
return Result.error("配置不存在");
}
return Result.success(SysConfigDTO.from(config));
}
/**
* 更新配置
*/
public Result<String> update(String key, String value) {
SysConfig config = configMapper.selectOne(
new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key));
if (config == null) {
return Result.error("配置不存在");
}
config.setConfigValue(value);
config.setUpdatedAt(new Date());
configMapper.updateById(config);
return Result.success("更新成功");
}
/**
* 内部方法:获取配置值(供其他 Service 调用)
*/
public String getValue(String key) {
SysConfig config = configMapper.selectOne(
new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key));
return config != null ? config.getConfigValue() : null;
}
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-backend/src/main/java/com/etotem/cfc/entity/SysConfig.java cfc-backend/src/main/java/com/etotem/cfc/mapper/SysConfigMapper.java cfc-backend/src/main/java/com/etotem/cfc/dto/SysConfigDTO.java cfc-backend/src/main/java/com/etotem/cfc/service/SysConfigService.java
git commit -m "feat(phase1): SysConfig 实体/Mapper/Service 实现"
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java
[ ] Step 1: 创建 ProductController.java
package com.etotem.cfc.controller.product;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.ProductDTO;
import com.etotem.cfc.dto.ProductListQueryDTO;
import com.etotem.cfc.entity.Product;
import com.etotem.cfc.service.ProductService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Resource
private ProductService productService;
/**
* 商品列表(公开)
*/
@PostMapping("/list")
public Result<Map<String, Object>> list(@RequestBody ProductListQueryDTO query) {
return productService.list(query);
}
/**
* 商品详情(公开)
*/
@PostMapping("/detail")
public Result<ProductDTO> detail(@RequestBody Map<String, Object> params) {
Long id = Long.parseLong(params.get("id").toString());
return productService.detail(id);
}
/**
* 服务商发布商品
*/
@PostMapping("/create")
public Result<ProductDTO> create(@RequestBody Product product,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
return productService.create(product, userId);
}
/**
* 服务商更新商品
*/
@PostMapping("/update")
public Result<ProductDTO> update(@RequestBody Product product,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
return productService.update(product, userId);
}
/**
* 服务商查询自己的商品
*/
@PostMapping("/my")
public Result<List<ProductDTO>> myProducts(@RequestHeader(value = "X-User-Id", required = false) Long userId) {
return productService.myProducts(userId);
}
/**
* 服务商上架/下架商品
*/
@PostMapping("/shelve")
public Result<String> shelve(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
Long productId = Long.parseLong(params.get("productId").toString());
String action = (String) params.get("action"); // shelve / unshelve
return productService.shelve(productId, userId, action);
}
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java
[ ] Step 1: 创建 ProductOrderController.java
package com.etotem.cfc.controller.product;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.CreateProductOrderDTO;
import com.etotem.cfc.dto.ProductOrderDTO;
import com.etotem.cfc.service.ProductOrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/product/order")
public class ProductOrderController {
@Resource
private ProductOrderService orderService;
/**
* 创建订单
*/
@PostMapping("/create")
public Result<ProductOrderDTO> create(@RequestBody CreateProductOrderDTO dto,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
return orderService.create(dto, userId);
}
/**
* 支付订单(测试模式)
*/
@PostMapping("/pay")
public Result<ProductOrderDTO> pay(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
String orderNo = (String) params.get("orderNo");
return orderService.pay(orderNo, userId);
}
/**
* 取消订单
*/
@PostMapping("/cancel")
public Result<String> cancel(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
String orderNo = (String) params.get("orderNo");
return orderService.cancel(orderNo, userId);
}
/**
* 我的订单
*/
@PostMapping("/my")
public Result<List<ProductOrderDTO>> myOrders(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
String status = (String) params.get("status");
return orderService.myOrders(userId, status);
}
/**
* 订单详情
*/
@PostMapping("/detail")
public Result<ProductOrderDTO> detail(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
String orderNo = (String) params.get("orderNo");
return orderService.detail(orderNo, userId);
}
/**
* 服务商查询收到的订单
*/
@PostMapping("/vendor")
public Result<List<ProductOrderDTO>> vendorOrders(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
String status = (String) params.get("status");
return orderService.vendorOrders(userId, status);
}
/**
* 服务商确认订单完成
*/
@PostMapping("/confirm")
public Result<String> confirm(@RequestBody Map<String, Object> params,
@RequestHeader(value = "X-User-Id", required = false) Long userId) {
String orderNo = (String) params.get("orderNo");
return orderService.confirm(orderNo, userId);
}
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Create: cfc-backend/src/main/java/com/etotem/cfc/controller/config/SysConfigController.java
[ ] Step 1: 创建 SysConfigController.java
package com.etotem.cfc.controller.config;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.SysConfigDTO;
import com.etotem.cfc.service.SysConfigService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin/config")
public class SysConfigController {
@Resource
private SysConfigService configService;
/**
* 管理员获取所有配置
*/
@PostMapping("/list")
public Result<List<SysConfigDTO>> list() {
return configService.listAll();
}
/**
* 管理员更新配置
*/
@PostMapping("/update")
public Result<String> update(@RequestBody Map<String, Object> params) {
String key = (String) params.get("configKey");
String value = (String) params.get("configValue");
return configService.update(key, value);
}
/**
* 内部使用:获取所有配置(key-value 形式)
*/
@PostMapping("/all")
public Result<Map<String, String>> getAllAsMap() {
return configService.getAllAsMap();
}
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
Files:
Modify: cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
[ ] Step 1: 找到 AdminController 类,在最后添加商品/订单审核端点
在类的最后一个方法之后(} 闭合大括号之前)添加:
@Resource
private com.etotem.cfc.service.ProductService productService;
@Resource
private com.etotem.cfc.service.ProductOrderService productOrderService;
@PostMapping("/product/pending")
public Result<List<com.etotem.cfc.dto.ProductDTO>> productPending() {
return productService.pendingReview();
}
@PostMapping("/product/list")
public Result<List<com.etotem.cfc.dto.ProductDTO>> productList(@RequestBody Map<String, Object> params) {
String status = (String) params.get("status");
return productService.adminList(status);
}
@PostMapping("/product/review")
public Result<String> productReview(@RequestBody Map<String, Object> params) {
Long productId = Long.parseLong(params.get("productId").toString());
String action = (String) params.get("action"); // approve / reject
String rejectReason = (String) params.get("rejectReason");
return productService.review(productId, action, rejectReason);
}
@PostMapping("/order/list")
public Result<List<com.etotem.cfc.dto.ProductOrderDTO>> orderList(@RequestBody Map<String, Object> params) {
String status = (String) params.get("status");
return productOrderService.adminList(status);
}
[ ] Step 2: 验证编译
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java cfc-backend/src/main/java/com/etotem/cfc/controller/config/SysConfigController.java cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
git commit -m "feat(phase1): Product/ProductOrder/SysConfig/Admin 后端 Controller"
Files:
Modify: cfc-frontend/utils/api.js
[ ] Step 1: 在 api.js 末尾追加商品/订单/系统配置 API
打开 cfc-frontend/utils/api.js,在 module.exports 之前找到合适位置(推荐放在 vendorApply 等 vendor API 附近),添加:
// ==================== Phase 1: 商品系统 ====================
// 商品列表(公开)
const productList = (params) => request('/api/product/list', 'POST', params);
// 商品详情(公开)
const productDetail = (params) => request('/api/product/detail', 'POST', params);
// 服务商发布商品
const productCreate = (params) => request('/api/product/create', 'POST', params);
// 服务商更新商品
const productUpdate = (params) => request('/api/product/update', 'POST', params);
// 服务商查询自己的商品
const productMy = (params) => request('/api/product/my', 'POST', params);
// 服务商上架/下架商品
const productShelve = (params) => request('/api/product/shelve', 'POST', params);
// 创建订单
const productOrderCreate = (params) => request('/api/product/order/create', 'POST', params);
// 支付订单
const productOrderPay = (params) => request('/api/product/order/pay', 'POST', params);
// 取消订单
const productOrderCancel = (params) => request('/api/product/order/cancel', 'POST', params);
// 我的订单
const productOrderMy = (params) => request('/api/product/order/my', 'POST', params);
// 订单详情
const productOrderDetail = (params) => request('/api/product/order/detail', 'POST', params);
// 服务商查询收到的订单
const productOrderVendor = (params) => request('/api/product/order/vendor', 'POST', params);
// 服务商确认订单完成
const productOrderConfirm = (params) => request('/api/product/order/confirm', 'POST', params);
// 管理员:商品待审核列表
const adminProductPending = (params) => request('/api/admin/product/pending', 'POST', params);
// 管理员:商品列表
const adminProductList = (params) => request('/api/admin/product/list', 'POST', params);
// 管理员:商品审核
const adminProductReview = (params) => request('/api/admin/product/review', 'POST', params);
// 管理员:订单列表
const adminOrderList = (params) => request('/api/admin/order/list', 'POST', params);
// 管理员:系统配置列表
const adminConfigList = (params) => request('/api/admin/config/list', 'POST', params);
// 管理员:更新系统配置
const adminConfigUpdate = (params) => request('/api/admin/config/update', 'POST', params);
module.exports,添加这些函数的导出在 module.exports 对象中追加:
productList, productDetail, productCreate, productUpdate, productMy, productShelve,
productOrderCreate, productOrderPay, productOrderCancel, productOrderMy, productOrderDetail, productOrderVendor, productOrderConfirm,
adminProductPending, adminProductList, adminProductReview, adminOrderList,
adminConfigList, adminConfigUpdate,
[ ] Step 3: 验证前端能 import 这些 API
cd /mnt/e/workspace/dan/zxyj/cfc-frontend && rtk grep "productList\|adminProductPending" -n 2>&1
Expected: 输出显示这些函数已经在 api.js 中定义和导出
[ ] Step 4: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/utils/api.js
git commit -m "feat(phase1): 前端 api.js 新增商品/订单/系统配置 API"
Files:
Modify: cfc-frontend/pages/shop/index.vue
[ ] Step 1: 完整重写 pages/shop/index.vue
用以下代码完整覆盖 cfc-frontend/pages/shop/index.vue:
<template>
<view class="shop-page">
<view class="header">
<view class="title">溪福商城</view>
<view class="subtitle">五行五维 · 选择属于你的能量</view>
</view>
<!-- 五行维度筛选 -->
<scroll-view class="domain-tabs" scroll-x>
<view v-for="d in domainOptions" :key="d.value"
:class="['tab', currentDomain === d.value ? 'active' : '']"
@click="onDomainChange(d.value)">
<text>{{ d.label }}</text>
</view>
</scroll-view>
<!-- 商品类型筛选 -->
<view class="type-filter">
<text v-for="t in typeOptions" :key="t.value"
:class="['filter-chip', currentType === t.value ? 'active' : '']"
@click="onTypeChange(t.value)">{{ t.label }}</text>
</view>
<!-- 商品网格 -->
<view class="product-grid">
<view v-for="p in products" :key="p.id" class="product-card" @click="goDetail(p.id)">
<image v-if="p.coverImage" :src="p.coverImage" mode="aspectFill" class="cover" />
<view v-else class="cover-placeholder">{{ domainEmoji(p.domain) }}</view>
<view class="info">
<view class="name">{{ p.name }}</view>
<view class="meta">
<text class="price">¥{{ p.price }}</text>
<text v-if="p.memberPrice" class="member-price">会员 ¥{{ p.memberPrice }}</text>
</view>
<view class="footer">
<text class="vendor">{{ p.vendorName || '平台官方' }}</text>
<text class="sales">已售 {{ p.salesCount || 0 }}</text>
</view>
</view>
</view>
</view>
<view v-if="!products.length && !loading" class="empty">暂无商品</view>
</view>
</template>
<script>
import { productList } from '@/utils/api.js';
export default {
data() {
return {
products: [],
loading: false,
currentDomain: '',
currentType: '',
domainOptions: [
{ label: '全部', value: '' },
{ label: '🌿 行·木', value: 'action' },
{ label: '🔥 心·火', value: 'mind' },
{ label: '🌏 身·土', value: 'body' },
{ label: '⚔️ 智·金', value: 'wisdom' },
{ label: '💧 富·水', value: 'wealth' }
],
typeOptions: [
{ label: '全部', value: '' },
{ label: '活动', value: 'activity' },
{ label: '课程', value: 'course' },
{ label: '实物', value: 'physical' },
{ label: '数字', value: 'digital' },
{ label: '服务', value: 'service' }
]
};
},
onShow() {
this.loadProducts();
},
methods: {
async loadProducts() {
this.loading = true;
try {
const res = await productList({
page: 1,
size: 50,
productType: this.currentType,
domain: this.currentDomain
});
if (res.code === 200) {
this.products = res.data.records || [];
}
} catch (e) {
console.error('loadProducts failed', e);
}
this.loading = false;
},
onDomainChange(value) {
this.currentDomain = value;
this.loadProducts();
},
onTypeChange(value) {
this.currentType = value;
this.loadProducts();
},
goDetail(id) {
uni.navigateTo({ url: '/pages/discover/product-detail?id=' + id });
},
domainEmoji(domain) {
const map = { action: '🌿', mind: '🔥', body: '🌏', wisdom: '⚔️', wealth: '💧' };
return map[domain] || '✨';
}
}
};
</script>
<style scoped>
.shop-page {
min-height: 100vh;
background: #F8F4ED;
padding: 20rpx 30rpx;
}
.header {
text-align: center;
margin: 20rpx 0 30rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #6B4423;
}
.subtitle {
font-size: 24rpx;
color: #8B7355;
margin-top: 8rpx;
}
.domain-tabs {
white-space: nowrap;
margin-bottom: 20rpx;
}
.tab {
display: inline-block;
padding: 16rpx 28rpx;
margin-right: 16rpx;
background: #FFFFFF;
border-radius: 30rpx;
font-size: 26rpx;
color: #6B4423;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
}
.tab.active {
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
}
.type-filter {
display: flex;
flex-wrap: wrap;
margin-bottom: 24rpx;
}
.filter-chip {
padding: 12rpx 24rpx;
margin: 8rpx 12rpx 8rpx 0;
background: #FFFFFF;
border-radius: 24rpx;
font-size: 24rpx;
color: #6B4423;
}
.filter-chip.active {
background: #FFD700;
color: #6B4423;
font-weight: bold;
}
.product-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.product-card {
background: #FFFFFF;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.cover {
width: 100%;
height: 280rpx;
display: block;
}
.cover-placeholder {
width: 100%;
height: 280rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 80rpx;
background: linear-gradient(135deg, #FFE5B4, #FFB88C);
}
.info {
padding: 16rpx;
}
.name {
font-size: 28rpx;
color: #3C2A1E;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 76rpx;
}
.meta {
display: flex;
align-items: baseline;
margin-top: 10rpx;
}
.price {
font-size: 32rpx;
color: #DE6262;
font-weight: bold;
}
.member-price {
font-size: 22rpx;
color: #FF8C00;
margin-left: 12rpx;
background: #FFF3D6;
padding: 2rpx 10rpx;
border-radius: 12rpx;
}
.footer {
display: flex;
justify-content: space-between;
margin-top: 8rpx;
font-size: 22rpx;
color: #8B7355;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #8B7355;
font-size: 28rpx;
}
</style>
[ ] Step 2: 验证文件保存成功
rtk wc -l /mnt/e/workspace/dan/zxyj/cfc-frontend/pages/shop/index.vue
Expected: 输出行数(应该 > 100)
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/shop/index.vue
git commit -m "feat(phase1): 商城页 - 从占位改为商品列表(五行筛选+类型筛选)"
Files:
Modify: cfc-frontend/pages/discover/index.vue
[ ] Step 1: 完整重写 pages/discover/index.vue
用以下代码完整覆盖 cfc-frontend/pages/discover/index.vue:
<template>
<view class="discover-page">
<view class="header">
<view class="title">溪福发现</view>
<view class="subtitle">活动 · 课程 · 探索</view>
</view>
<!-- 类型切换 -->
<view class="type-tabs">
<text v-for="t in typeOptions" :key="t.value"
:class="['tab', currentType === t.value ? 'active' : '']"
@click="onTypeChange(t.value)">{{ t.label }}</text>
</view>
<!-- 列表 -->
<view class="discover-list">
<view v-for="p in products" :key="p.id" class="card" @click="goDetail(p.id)">
<view class="card-left">
<view class="card-cover">{{ domainEmoji(p.domain) }}</view>
</view>
<view class="card-right">
<view class="card-title">{{ p.name }}</view>
<view class="card-desc">{{ p.description }}</view>
<view class="card-meta">
<text class="card-price">¥{{ p.price }}</text>
<text class="card-vendor">{{ p.vendorName || '平台官方' }}</text>
</view>
</view>
</view>
</view>
<view v-if="!products.length && !loading" class="empty">暂无内容</view>
</view>
</template>
<script>
import { productList } from '@/utils/api.js';
export default {
data() {
return {
products: [],
loading: false,
currentType: '',
typeOptions: [
{ label: '推荐', value: '' },
{ label: '活动', value: 'activity' },
{ label: '课程', value: 'course' }
]
};
},
onShow() {
this.loadProducts();
},
methods: {
async loadProducts() {
this.loading = true;
try {
const res = await productList({
page: 1,
size: 30,
productType: this.currentType
});
if (res.code === 200) {
this.products = res.data.records || [];
}
} catch (e) {
console.error('loadProducts failed', e);
}
this.loading = false;
},
onTypeChange(value) {
this.currentType = value;
this.loadProducts();
},
goDetail(id) {
uni.navigateTo({ url: '/pages/discover/product-detail?id=' + id });
},
domainEmoji(domain) {
const map = { action: '🌿', mind: '🔥', body: '🌏', wisdom: '⚔️', wealth: '💧' };
return map[domain] || '✨';
}
}
};
</script>
<style scoped>
.discover-page {
min-height: 100vh;
background: #F8F4ED;
padding: 20rpx 30rpx;
}
.header {
text-align: center;
margin: 20rpx 0 30rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #6B4423;
}
.subtitle {
font-size: 24rpx;
color: #8B7355;
margin-top: 8rpx;
}
.type-tabs {
display: flex;
margin-bottom: 24rpx;
}
.tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
background: #FFFFFF;
margin: 0 8rpx;
border-radius: 20rpx;
font-size: 26rpx;
color: #6B4423;
}
.tab.active {
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
}
.discover-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.card {
display: flex;
background: #FFFFFF;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
padding: 20rpx;
}
.card-left {
flex-shrink: 0;
}
.card-cover {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #FFE5B4, #FFB88C);
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
}
.card-right {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-title {
font-size: 30rpx;
font-weight: 500;
color: #3C2A1E;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-desc {
font-size: 24rpx;
color: #8B7355;
margin-top: 6rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10rpx;
}
.card-price {
font-size: 30rpx;
color: #DE6262;
font-weight: bold;
}
.card-vendor {
font-size: 22rpx;
color: #8B7355;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #8B7355;
font-size: 28rpx;
}
</style>
[ ] Step 2: 验证文件保存成功
rtk wc -l /mnt/e/workspace/dan/zxyj/cfc-frontend/pages/discover/index.vue
Expected: 输出行数(应该 > 100)
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/discover/index.vue
git commit -m "feat(phase1): 发现页 - 从占位改为活动+课程列表"
Files:
cfc-frontend/pages/discover/product-detail.vueModify: cfc-frontend/pages.json(注册新页面)
[ ] Step 1: 创建 pages/discover/product-detail.vue
<template>
<view class="detail-page">
<view v-if="product" class="content">
<view class="cover-section">
<view v-if="product.coverImage" class="cover">
<image :src="product.coverImage" mode="aspectFill" class="cover-img" />
</view>
<view v-else class="cover cover-placeholder">
<text class="cover-emoji">{{ domainEmoji(product.domain) }}</text>
</view>
</view>
<view class="info-section">
<view class="name">{{ product.name }}</view>
<view class="price-row">
<text class="price">¥{{ product.price }}</text>
<text v-if="product.memberPrice" class="member-price">会员价 ¥{{ product.memberPrice }}</text>
</view>
<view class="vendor">由 {{ product.vendorName || '平台官方' }} 提供</view>
<view v-if="product.description" class="description">{{ product.description }}</view>
</view>
<view class="action-bar">
<button class="btn-buy" @click="onBuy">立即购买</button>
</view>
</view>
<view v-else class="loading">加载中...</view>
</view>
</template>
<script>
import { productDetail, productOrderCreate, productOrderPay } from '@/utils/api.js';
export default {
data() {
return {
product: null,
productId: null
};
},
onLoad(options) {
this.productId = options.id;
this.loadDetail();
},
methods: {
async loadDetail() {
try {
const res = await productDetail({ id: this.productId });
if (res.code === 200) {
this.product = res.data;
} else {
uni.showToast({ title: res.message || '加载失败', icon: 'none' });
}
} catch (e) {
console.error('loadDetail failed', e);
}
},
async onBuy() {
if (!this.product) return;
uni.showLoading({ title: '创建订单中...' });
try {
const createRes = await productOrderCreate({
productId: this.product.id,
quantity: 1,
paymentMethod: 'points'
});
if (createRes.code !== 200) {
uni.hideLoading();
uni.showToast({ title: createRes.message || '创建订单失败', icon: 'none' });
return;
}
const orderNo = createRes.data.orderNo;
// 直接支付(测试模式)
const payRes = await productOrderPay({ orderNo: orderNo });
uni.hideLoading();
if (payRes.code === 200) {
uni.showToast({ title: '购买成功', icon: 'success' });
setTimeout(() => {
uni.redirectTo({ url: '/pages/shop/order-detail?orderNo=' + orderNo });
}, 1000);
} else {
uni.showToast({ title: payRes.message || '支付失败', icon: 'none' });
}
} catch (e) {
uni.hideLoading();
console.error('onBuy failed', e);
uni.showToast({ title: '网络错误', icon: 'none' });
}
},
domainEmoji(domain) {
const map = { action: '🌿', mind: '🔥', body: '🌏', wisdom: '⚔️', wealth: '💧' };
return map[domain] || '✨';
}
}
};
</script>
<style scoped>
.detail-page {
min-height: 100vh;
background: #F8F4ED;
padding-bottom: 120rpx;
}
.cover-section {
width: 100%;
height: 500rpx;
}
.cover {
width: 100%;
height: 500rpx;
}
.cover-img {
width: 100%;
height: 100%;
}
.cover-placeholder {
background: linear-gradient(135deg, #FFE5B4, #FFB88C);
display: flex;
align-items: center;
justify-content: center;
}
.cover-emoji {
font-size: 200rpx;
}
.info-section {
background: #FFFFFF;
padding: 30rpx;
border-radius: 30rpx 30rpx 0 0;
margin-top: -30rpx;
position: relative;
}
.name {
font-size: 36rpx;
font-weight: bold;
color: #3C2A1E;
line-height: 1.4;
}
.price-row {
display: flex;
align-items: baseline;
margin-top: 20rpx;
}
.price {
font-size: 44rpx;
color: #DE6262;
font-weight: bold;
}
.member-price {
font-size: 26rpx;
color: #FF8C00;
margin-left: 16rpx;
background: #FFF3D6;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.vendor {
font-size: 24rpx;
color: #8B7355;
margin-top: 12rpx;
}
.description {
margin-top: 24rpx;
font-size: 28rpx;
color: #3C2A1E;
line-height: 1.6;
white-space: pre-wrap;
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.08);
}
.btn-buy {
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
border: none;
border-radius: 50rpx;
font-size: 30rpx;
font-weight: bold;
height: 88rpx;
line-height: 88rpx;
}
.loading {
text-align: center;
padding: 200rpx 0;
color: #8B7355;
font-size: 28rpx;
}
</style>
[ ] Step 2: 在 pages.json 中注册新页面
打开 cfc-frontend/pages.json,找到 pages 数组,在最后添加:
{
"path": "pages/discover/product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
[ ] Step 3: 验证文件存在
rtk test -f /mnt/e/workspace/dan/zxyj/cfc-frontend/pages/discover/product-detail.vue
rtk grep "product-detail" /mnt/e/workspace/dan/zxyj/cfc-frontend/pages.json
Expected: 第二个命令输出包含 "pages/discover/product-detail"
[ ] Step 4: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/discover/product-detail.vue cfc-frontend/pages.json
git commit -m "feat(phase1): 商品详情页(商城/发现共用)"
Files:
cfc-frontend/pages/shop/order-list.vueModify: cfc-frontend/pages.json
[ ] Step 1: 创建 pages/shop/order-list.vue
<template>
<view class="order-list-page">
<view class="tabs">
<text v-for="t in statusOptions" :key="t.value"
:class="['tab', currentStatus === t.value ? 'active' : '']"
@click="onTabChange(t.value)">{{ t.label }}</text>
</view>
<view v-if="orders.length === 0 && !loading" class="empty">暂无订单</view>
<view class="list">
<view v-for="o in orders" :key="o.id" class="order-card" @click="goDetail(o.orderNo)">
<view class="order-header">
<text class="order-no">订单号:{{ o.orderNo }}</text>
<text :class="['status', 'status-' + o.status]">{{ statusLabel(o.status) }}</text>
</view>
<view class="order-body">
<view class="product-name">{{ o.productName }}</view>
<view class="order-meta">
<text class="quantity">x{{ o.quantity }}</text>
<text class="amount">¥{{ o.totalAmount }}</text>
</view>
</view>
<view class="order-time">{{ formatTime(o.createdAt) }}</view>
</view>
</view>
</view>
</template>
<script>
import { productOrderMy } from '@/utils/api.js';
export default {
data() {
return {
orders: [],
loading: false,
currentStatus: '',
statusOptions: [
{ label: '全部', value: '' },
{ label: '待支付', value: 'pending' },
{ label: '已支付', value: 'paid' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
]
};
},
onShow() {
this.loadOrders();
},
methods: {
async loadOrders() {
this.loading = true;
try {
const res = await productOrderMy({ status: this.currentStatus });
if (res.code === 200) {
this.orders = res.data || [];
}
} catch (e) {
console.error('loadOrders failed', e);
}
this.loading = false;
},
onTabChange(value) {
this.currentStatus = value;
this.loadOrders();
},
goDetail(orderNo) {
uni.navigateTo({ url: '/pages/shop/order-detail?orderNo=' + orderNo });
},
statusLabel(s) {
const map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
return map[s] || s;
},
formatTime(t) {
if (!t) return '';
const d = new Date(t);
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes();
}
}
};
</script>
<style scoped>
.order-list-page {
min-height: 100vh;
background: #F8F4ED;
padding: 20rpx 30rpx;
}
.tabs {
display: flex;
background: #FFFFFF;
border-radius: 20rpx;
padding: 10rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
}
.tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 26rpx;
color: #6B4423;
border-radius: 16rpx;
}
.tab.active {
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #8B7355;
font-size: 28rpx;
}
.list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.order-card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #F0E5D8;
}
.order-no {
font-size: 22rpx;
color: #8B7355;
}
.status {
font-size: 24rpx;
font-weight: bold;
}
.status-pending { color: #FF8C00; }
.status-paid { color: #4CAF50; }
.status-completed { color: #2196F3; }
.status-cancelled { color: #999; }
.status-refunded { color: #F44336; }
.order-body {
padding: 20rpx 0;
}
.product-name {
font-size: 28rpx;
color: #3C2A1E;
font-weight: 500;
}
.order-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12rpx;
}
.quantity {
font-size: 24rpx;
color: #8B7355;
}
.amount {
font-size: 32rpx;
color: #DE6262;
font-weight: bold;
}
.order-time {
font-size: 22rpx;
color: #8B7355;
text-align: right;
}
</style>
[ ] Step 2: 在 pages.json 中注册新页面
打开 cfc-frontend/pages.json,在 pages 数组最后添加:
{
"path": "pages/shop/order-list",
"style": {
"navigationBarTitleText": "我的订单"
}
},
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/shop/order-list.vue cfc-frontend/pages.json
git commit -m "feat(phase1): 订单列表页"
Files:
cfc-frontend/pages/shop/order-detail.vueModify: cfc-frontend/pages.json
[ ] Step 1: 创建 pages/shop/order-detail.vue
<template>
<view class="order-detail-page">
<view v-if="order">
<view class="status-section" :class="'status-' + order.status">
<text class="status-text">{{ statusLabel(order.status) }}</text>
</view>
<view class="section">
<view class="row">
<text class="label">商品</text>
<text class="value">{{ order.productName }}</text>
</view>
<view class="row">
<text class="label">数量</text>
<text class="value">x{{ order.quantity }}</text>
</view>
<view class="row">
<text class="label">单价</text>
<text class="value">¥{{ order.unitPrice }}</text>
</view>
<view class="row">
<text class="label">订单号</text>
<text class="value">{{ order.orderNo }}</text>
</view>
<view v-if="order.paidAt" class="row">
<text class="label">支付时间</text>
<text class="value">{{ formatTime(order.paidAt) }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ formatTime(order.createdAt) }}</text>
</view>
</view>
<view class="amount-section">
<text class="amount-label">实付金额</text>
<text class="amount-value">¥{{ order.totalAmount }}</text>
</view>
<view v-if="order.status === 'pending'" class="action-bar">
<button class="btn-cancel" @click="onCancel">取消订单</button>
<button class="btn-pay" @click="onPay">立即支付</button>
</view>
</view>
<view v-else class="loading">加载中...</view>
</view>
</template>
<script>
import { productOrderDetail, productOrderPay, productOrderCancel } from '@/utils/api.js';
export default {
data() {
return {
order: null,
orderNo: null
};
},
onLoad(options) {
this.orderNo = options.orderNo;
this.loadDetail();
},
methods: {
async loadDetail() {
try {
const res = await productOrderDetail({ orderNo: this.orderNo });
if (res.code === 200) {
this.order = res.data;
} else {
uni.showToast({ title: res.message || '加载失败', icon: 'none' });
}
} catch (e) {
console.error('loadDetail failed', e);
}
},
async onPay() {
uni.showLoading({ title: '支付中...' });
try {
const res = await productOrderPay({ orderNo: this.orderNo });
uni.hideLoading();
if (res.code === 200) {
uni.showToast({ title: '支付成功', icon: 'success' });
this.loadDetail();
} else {
uni.showToast({ title: res.message || '支付失败', icon: 'none' });
}
} catch (e) {
uni.hideLoading();
console.error('onPay failed', e);
}
},
async onCancel() {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: async (modalRes) => {
if (modalRes.confirm) {
try {
const res = await productOrderCancel({ orderNo: this.orderNo });
if (res.code === 200) {
uni.showToast({ title: '已取消', icon: 'success' });
this.loadDetail();
} else {
uni.showToast({ title: res.message || '取消失败', icon: 'none' });
}
} catch (e) {
console.error('onCancel failed', e);
}
}
}
});
},
statusLabel(s) {
const map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
return map[s] || s;
},
formatTime(t) {
if (!t) return '';
const d = new Date(t);
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes();
}
}
};
</script>
<style scoped>
.order-detail-page {
min-height: 100vh;
background: #F8F4ED;
padding-bottom: 140rpx;
}
.status-section {
padding: 40rpx 30rpx;
text-align: center;
color: #FFFFFF;
}
.status-pending { background: linear-gradient(135deg, #FFB88C, #FF8C00); }
.status-paid { background: linear-gradient(135deg, #81C784, #4CAF50); }
.status-completed { background: linear-gradient(135deg, #64B5F6, #2196F3); }
.status-cancelled { background: linear-gradient(135deg, #BDBDBD, #9E9E9E); }
.status-refunded { background: linear-gradient(135deg, #E57373, #F44336); }
.status-text {
font-size: 36rpx;
font-weight: bold;
}
.section {
background: #FFFFFF;
margin: 20rpx 30rpx;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.row {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 28rpx;
border-bottom: 1rpx solid #F5F0E8;
}
.row:last-child {
border-bottom: none;
}
.label {
color: #8B7355;
}
.value {
color: #3C2A1E;
}
.amount-section {
background: #FFFFFF;
margin: 20rpx 30rpx;
border-radius: 20rpx;
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.amount-label {
font-size: 28rpx;
color: #6B4423;
}
.amount-value {
font-size: 40rpx;
color: #DE6262;
font-weight: bold;
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: #FFFFFF;
padding: 20rpx 30rpx;
gap: 20rpx;
box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.08);
}
.btn-cancel {
flex: 1;
background: #FFFFFF;
color: #6B4423;
border: 2rpx solid #6B4423;
border-radius: 50rpx;
font-size: 30rpx;
height: 88rpx;
line-height: 84rpx;
}
.btn-pay {
flex: 2;
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
border: none;
border-radius: 50rpx;
font-size: 30rpx;
font-weight: bold;
height: 88rpx;
line-height: 88rpx;
}
.loading {
text-align: center;
padding: 200rpx 0;
color: #8B7355;
font-size: 28rpx;
}
</style>
[ ] Step 2: 在 pages.json 中注册新页面
打开 cfc-frontend/pages.json,在 pages 数组最后添加:
{
"path": "pages/shop/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/shop/order-detail.vue cfc-frontend/pages.json
git commit -m "feat(phase1): 订单详情页"
Files:
cfc-frontend/pages/vendor/products.vueModify: cfc-frontend/pages.json
[ ] Step 1: 创建 pages/vendor/products.vue
<template>
<view class="vendor-products-page">
<view class="header">
<view class="title">我的商品</view>
<button class="btn-add" @click="onCreate">+ 发布商品</button>
</view>
<view v-if="!isVendor" class="not-vendor">
<text>请先入驻成为服务商</text>
<button @click="goApply">立即入驻</button>
</view>
<view v-else>
<view v-if="products.length === 0" class="empty">尚未发布商品</view>
<view v-else class="list">
<view v-for="p in products" :key="p.id" class="card">
<view class="card-header">
<text class="name">{{ p.name }}</text>
<text :class="['status-tag', 'status-' + p.status]">{{ statusLabel(p.status) }}</text>
</view>
<view class="card-info">
<text class="price">¥{{ p.price }}</text>
<text class="stock">库存:{{ p.stock }}</text>
<text class="sales">已售:{{ p.salesCount || 0 }}</text>
</view>
<view v-if="p.rejectReason" class="reject-reason">拒绝原因:{{ p.rejectReason }}</view>
<view class="actions">
<button v-if="p.status === 'approved' && p.status !== 'on_shelf'" class="btn-act" @click="onShelve(p, 'shelve')">上架</button>
<button v-if="p.status === 'on_shelf'" class="btn-act" @click="onShelve(p, 'unshelve')">下架</button>
<button class="btn-act" @click="onEdit(p)">编辑</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { productMy, productShelve, vendorStatus } from '@/utils/api.js';
export default {
data() {
return {
products: [],
isVendor: false
};
},
onShow() {
this.checkVendor();
},
methods: {
async checkVendor() {
try {
const res = await vendorStatus();
if (res.code === 200 && res.data && res.data.vendorStatus === 'approved') {
this.isVendor = true;
this.loadProducts();
} else {
this.isVendor = false;
}
} catch (e) {
console.error('checkVendor failed', e);
}
},
async loadProducts() {
try {
const res = await productMy({});
if (res.code === 200) {
this.products = res.data || [];
}
} catch (e) {
console.error('loadProducts failed', e);
}
},
onCreate() {
uni.showModal({
title: '发布商品',
content: '商品发布功能:\n1. 名称、价格、库存、类型\n2. 提交后需管理员审核\n\n请通过管理端/Web 端发布,或在后续 Phase 完善。',
showCancel: false
});
},
onEdit(p) {
uni.showToast({ title: '编辑功能待实现', icon: 'none' });
},
async onShelve(p, action) {
try {
const res = await productShelve({ productId: p.id, action: action });
if (res.code === 200) {
uni.showToast({ title: action === 'shelve' ? '已上架' : '已下架', icon: 'success' });
this.loadProducts();
} else {
uni.showToast({ title: res.message || '操作失败', icon: 'none' });
}
} catch (e) {
console.error('onShelve failed', e);
}
},
goApply() {
uni.navigateTo({ url: '/pages/vendor/apply' });
},
statusLabel(s) {
const map = {
draft: '草稿', pending: '待审核', approved: '已通过', rejected: '已拒绝',
on_shelf: '已上架', off_shelf: '已下架'
};
return map[s] || s;
}
}
};
</script>
<style scoped>
.vendor-products-page {
min-height: 100vh;
background: #F8F4ED;
padding: 20rpx 30rpx;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #6B4423;
}
.btn-add {
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
border: none;
border-radius: 30rpx;
font-size: 26rpx;
padding: 12rpx 24rpx;
height: auto;
line-height: 1.4;
}
.not-vendor {
text-align: center;
padding: 100rpx 0;
color: #8B7355;
}
.not-vendor button {
margin-top: 30rpx;
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
border: none;
border-radius: 50rpx;
width: 60%;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #8B7355;
font-size: 28rpx;
}
.list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.name {
font-size: 30rpx;
color: #3C2A1E;
font-weight: 500;
}
.status-tag {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.status-pending { background: #FFF3D6; color: #FF8C00; }
.status-approved { background: #D6F5D6; color: #4CAF50; }
.status-rejected { background: #FFD6D6; color: #F44336; }
.status-on_shelf { background: #D6E5F5; color: #2196F3; }
.status-off_shelf { background: #E0E0E0; color: #757575; }
.card-info {
display: flex;
gap: 20rpx;
margin-top: 12rpx;
font-size: 24rpx;
color: #8B7355;
}
.price {
color: #DE6262;
font-weight: bold;
font-size: 28rpx;
}
.reject-reason {
margin-top: 12rpx;
font-size: 22rpx;
color: #F44336;
background: #FFF0F0;
padding: 8rpx 12rpx;
border-radius: 8rpx;
}
.actions {
display: flex;
gap: 16rpx;
margin-top: 20rpx;
border-top: 1rpx solid #F0E5D8;
padding-top: 16rpx;
}
.btn-act {
flex: 1;
background: #F8F4ED;
color: #6B4423;
border: 1rpx solid #E0D5C0;
border-radius: 20rpx;
font-size: 24rpx;
height: 60rpx;
line-height: 60rpx;
}
</style>
[ ] Step 2: 在 pages.json 中注册新页面
打开 cfc-frontend/pages.json,在 pages 数组最后添加:
{
"path": "pages/vendor/products",
"style": {
"navigationBarTitleText": "商品管理"
}
},
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/vendor/products.vue cfc-frontend/pages.json
git commit -m "feat(phase1): 服务商商品管理页"
Files:
cfc-frontend/pages/vendor/orders.vueModify: cfc-frontend/pages.json
[ ] Step 1: 创建 pages/vendor/orders.vue
<template>
<view class="vendor-orders-page">
<view class="header">
<view class="title">收到的订单</view>
</view>
<view v-if="orders.length === 0 && !loading" class="empty">暂无订单</view>
<view v-else class="list">
<view v-for="o in orders" :key="o.id" class="card">
<view class="card-header">
<text class="order-no">{{ o.orderNo }}</text>
<text :class="['status', 'status-' + o.status]">{{ statusLabel(o.status) }}</text>
</view>
<view class="card-body">
<view class="product-name">{{ o.productName }}</view>
<view class="meta">
<text class="quantity">x{{ o.quantity }}</text>
<text class="amount">¥{{ o.totalAmount }}</text>
</view>
</view>
<view class="card-actions">
<button v-if="o.status === 'paid'" class="btn-confirm" @click="onConfirm(o)">确认完成</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { productOrderVendor, productOrderConfirm } from '@/utils/api.js';
export default {
data() {
return {
orders: [],
loading: false
};
},
onShow() {
this.loadOrders();
},
methods: {
async loadOrders() {
this.loading = true;
try {
const res = await productOrderVendor({});
if (res.code === 200) {
this.orders = res.data || [];
}
} catch (e) {
console.error('loadOrders failed', e);
}
this.loading = false;
},
async onConfirm(o) {
try {
const res = await productOrderConfirm({ orderNo: o.orderNo });
if (res.code === 200) {
uni.showToast({ title: '已确认', icon: 'success' });
this.loadOrders();
} else {
uni.showToast({ title: res.message || '操作失败', icon: 'none' });
}
} catch (e) {
console.error('onConfirm failed', e);
}
},
statusLabel(s) {
const map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
return map[s] || s;
}
}
};
</script>
<style scoped>
.vendor-orders-page {
min-height: 100vh;
background: #F8F4ED;
padding: 20rpx 30rpx;
}
.header {
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #6B4423;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #8B7355;
font-size: 28rpx;
}
.list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.card {
background: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12rpx;
border-bottom: 1rpx solid #F0E5D8;
}
.order-no {
font-size: 22rpx;
color: #8B7355;
}
.status {
font-size: 24rpx;
font-weight: bold;
}
.status-pending { color: #FF8C00; }
.status-paid { color: #4CAF50; }
.status-completed { color: #2196F3; }
.status-cancelled { color: #999; }
.card-body {
padding: 16rpx 0;
}
.product-name {
font-size: 28rpx;
color: #3C2A1E;
font-weight: 500;
}
.meta {
display: flex;
justify-content: space-between;
margin-top: 8rpx;
}
.quantity {
font-size: 24rpx;
color: #8B7355;
}
.amount {
font-size: 30rpx;
color: #DE6262;
font-weight: bold;
}
.card-actions {
border-top: 1rpx solid #F0E5D8;
padding-top: 16rpx;
}
.btn-confirm {
width: 100%;
background: linear-gradient(135deg, #FFB88C, #DE6262);
color: #FFFFFF;
border: none;
border-radius: 30rpx;
font-size: 26rpx;
height: 70rpx;
line-height: 70rpx;
}
</style>
[ ] Step 2: 在 pages.json 中注册新页面
打开 cfc-frontend/pages.json,在 pages 数组最后添加:
{
"path": "pages/vendor/orders",
"style": {
"navigationBarTitleText": "订单管理"
}
},
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/vendor/orders.vue cfc-frontend/pages.json
git commit -m "feat(phase1): 服务商订单管理页"
Files:
Modify: cfc-frontend/pages/vendor/center.vue
[ ] Step 1: 找到 vendor/center.vue 中"菜单项"区域
打开 cfc-frontend/pages/vendor/center.vue,找到 menu-items 列表(如 <view class="menu">...</view> 或类似),在每个菜单项 <view class="menu-item"> 之后添加新菜单项。
如果菜单项使用 methods.goXxx,在 methods 中添加:
goProductManage() {
uni.navigateTo({ url: '/pages/vendor/products' });
},
goOrderManage() {
uni.navigateTo({ url: '/pages/vendor/orders' });
},
具体位置请根据现有 center.vue 的代码结构进行调整。参考现有代码(已在 Phase 0 实现)。
[ ] Step 2: 验证文件被修改
rtk grep -c "goProductManage\|goOrderManage" /mnt/e/workspace/dan/zxyj/cfc-frontend/pages/vendor/center.vue
Expected: 输出 >= 1(说明有新增的菜单项处理函数)
[ ] Step 3: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-frontend/pages/vendor/center.vue
git commit -m "feat(phase1): 服务商中心添加商品管理/订单管理入口"
Files:
Create: cfc-web/src/api/product.js
[ ] Step 1: 创建 src/api/product.js
import request from '@/utils/request';
// 管理员:商品待审核列表
export const adminProductPending = (params) => request({ url: '/api/admin/product/pending', method: 'post', data: params });
// 管理员:商品列表
export const adminProductList = (params) => request({ url: '/api/admin/product/list', method: 'post', data: params });
// 管理员:商品审核
export const adminProductReview = (params) => request({ url: '/api/admin/product/review', method: 'post', data: params });
// 商品列表(公开)
export const productList = (params) => request({ url: '/api/product/list', method: 'post', data: params });
[ ] Step 2: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-web/src/api/product.js
git commit -m "feat(phase1): Web 端 product API 封装"
Files:
Create: cfc-web/src/views/admin/ProductManage.vue
[ ] Step 1: 创建 src/views/admin/ProductManage.vue
<template>
<div class="product-manage">
<el-card>
<div slot="header" class="clearfix">
<span>商品管理</span>
<el-select v-model="statusFilter" placeholder="按状态筛选" clearable style="width: 200px; margin-left: 20px;" @change="loadList">
<el-option label="待审核" value="pending" />
<el-option label="已通过" value="approved" />
<el-option label="已上架" value="on_shelf" />
<el-option label="已下架" value="off_shelf" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</div>
<el-table :data="products" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="商品名称" min-width="200" />
<el-table-column prop="productType" label="类型" width="100">
<template slot-scope="scope">
<el-tag>{{ typeLabel(scope.row.productType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="price" label="价格" width="100">
<template slot-scope="scope">¥{{ scope.row.price }}</template>
</el-table-column>
<el-table-column prop="memberPrice" label="会员价" width="100">
<template slot-scope="scope">
<span v-if="scope.row.memberPrice">¥{{ scope.row.memberPrice }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="stock" label="库存" width="80" />
<el-table-column prop="salesCount" label="销量" width="80" />
<el-table-column prop="vendorName" label="服务商" width="150" />
<el-table-column prop="status" label="状态" width="120">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button v-if="scope.row.status === 'pending'" size="mini" type="success" @click="onReview(scope.row, 'approve')">通过</el-button>
<el-button v-if="scope.row.status === 'pending'" size="mini" type="danger" @click="onReview(scope.row, 'reject')">拒绝</el-button>
<el-button v-if="scope.row.status === 'rejected' && scope.row.rejectReason" size="mini" @click="showRejectReason(scope.row.rejectReason)">查看原因</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="拒绝原因" :visible.sync="rejectDialogVisible" width="500px">
<el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入拒绝原因" />
<div slot="footer">
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmReject">确认拒绝</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { adminProductList, adminProductReview } from '@/api/product';
export default {
data() {
return {
products: [],
statusFilter: '',
rejectDialogVisible: false,
rejectReason: '',
currentRejectProduct: null
};
},
mounted() {
this.loadList();
},
methods: {
async loadList() {
try {
const res = await adminProductList({ status: this.statusFilter });
if (res.data && res.data.code === 200) {
this.products = res.data.data || [];
}
} catch (e) {
this.$message.error('加载失败');
}
},
onReview(product, action) {
if (action === 'approve') {
this.$confirm('确认通过此商品审核?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await this.submitReview(product, 'approve', null);
}).catch(() => {});
} else {
this.currentRejectProduct = product;
this.rejectReason = '';
this.rejectDialogVisible = true;
}
},
async confirmReject() {
if (!this.rejectReason.trim()) {
this.$message.warning('请输入拒绝原因');
return;
}
await this.submitReview(this.currentRejectProduct, 'reject', this.rejectReason);
this.rejectDialogVisible = false;
},
async submitReview(product, action, rejectReason) {
try {
const res = await adminProductReview({
productId: product.id,
action: action,
rejectReason: rejectReason
});
if (res.data && res.data.code === 200) {
this.$message.success('操作成功');
this.loadList();
} else {
this.$message.error(res.data.message || '操作失败');
}
} catch (e) {
this.$message.error('操作失败');
}
},
showRejectReason(reason) {
this.$alert(reason, '拒绝原因', { confirmButtonText: '关闭' });
},
typeLabel(type) {
const map = { activity: '活动', course: '课程', physical: '实物', digital: '数字', service: '服务' };
return map[type] || type;
},
statusLabel(s) {
const map = { draft: '草稿', pending: '待审核', approved: '已通过', rejected: '已拒绝', on_shelf: '已上架', off_shelf: '已下架' };
return map[s] || s;
},
statusTagType(s) {
const map = { pending: 'warning', approved: 'success', rejected: 'danger', on_shelf: 'success', off_shelf: 'info' };
return map[s] || '';
}
}
};
</script>
<style scoped>
.product-manage {
padding: 20px;
}
</style>
[ ] Step 2: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-web/src/views/admin/ProductManage.vue
git commit -m "feat(phase1): Web 端商品管理页面"
Files:
cfc-web/src/router/index.jsModify: cfc-web/src/views/Layout.vue
[ ] Step 1: 在 router/index.js 添加商品管理路由
打开 cfc-web/src/router/index.js,找到路由数组(如 adminRoutes 或类似),添加:
{
path: '/admin/product',
component: () => import('@/views/admin/ProductManage.vue'),
meta: { title: '商品管理', icon: 'goods' }
}
打开 cfc-web/src/views/Layout.vue,找到菜单列表,在"系统"或"运营"子菜单下添加:
<el-menu-item index="/admin/product">
<i class="el-icon-goods"></i>
<span slot="title">商品管理</span>
</el-menu-item>
具体位置请根据现有 Layout.vue 的菜单结构调整。
[ ] Step 3: 验证文件修改
rtk grep "ProductManage\|商品管理" /mnt/e/workspace/dan/zxyj/cfc-web/src/router/index.js
rtk grep "商品管理" /mnt/e/workspace/dan/zxyj/cfc-web/src/views/Layout.vue
Expected: 两个 grep 都有输出
[ ] Step 4: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-web/src/router/index.js cfc-web/src/views/Layout.vue
git commit -m "feat(phase1): Web 端商品管理路由 + 菜单"
Files:
cfc-web/src/views/admin/OrderManage.vuecfc-web/src/router/index.jsModify: cfc-web/src/views/Layout.vue
[ ] Step 1: 创建 src/views/admin/OrderManage.vue
<template>
<div class="order-manage">
<el-card>
<div slot="header" class="clearfix">
<span>订单管理</span>
<el-select v-model="statusFilter" placeholder="按状态筛选" clearable style="width: 200px; margin-left: 20px;" @change="loadList">
<el-option label="待支付" value="pending" />
<el-option label="已支付" value="paid" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</div>
<el-table :data="orders" border style="width: 100%">
<el-table-column prop="orderNo" label="订单号" min-width="200" />
<el-table-column prop="productName" label="商品" min-width="200" />
<el-table-column prop="buyerId" label="买家ID" width="100" />
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column prop="totalAmount" label="金额" width="100">
<template slot-scope="scope">¥{{ scope.row.totalAmount }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template slot-scope="scope">
<el-tag :type="statusTagType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="paymentMethod" label="支付方式" width="120" />
<el-table-column prop="createdAt" label="创建时间" width="180" />
</el-table>
</el-card>
</div>
</template>
<script>
import { adminOrderList } from '@/api/product';
export default {
data() {
return {
orders: [],
statusFilter: ''
};
},
mounted() {
this.loadList();
},
methods: {
async loadList() {
try {
const res = await adminOrderList({ status: this.statusFilter });
if (res.data && res.data.code === 200) {
this.orders = res.data.data || [];
}
} catch (e) {
this.$message.error('加载失败');
}
},
statusLabel(s) {
const map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' };
return map[s] || s;
},
statusTagType(s) {
const map = { pending: 'warning', paid: 'primary', completed: 'success', cancelled: 'info', refunded: 'danger' };
return map[s] || '';
}
}
};
</script>
<style scoped>
.order-manage {
padding: 20px;
}
</style>
[ ] Step 2: 添加路由
在 cfc-web/src/router/index.js 中添加:
{
path: '/admin/order',
component: () => import('@/views/admin/OrderManage.vue'),
meta: { title: '订单管理', icon: 'list' }
}
在 cfc-web/src/views/Layout.vue 菜单中"商品管理"附近添加:
<el-menu-item index="/admin/order">
<i class="el-icon-list"></i>
<span slot="title">订单管理</span>
</el-menu-item>
[ ] Step 4: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-web/src/views/admin/OrderManage.vue cfc-web/src/router/index.js cfc-web/src/views/Layout.vue
git commit -m "feat(phase1): Web 端订单管理页面+路由+菜单"
Files:
cfc-web/src/views/admin/SysConfig.vuecfc-web/src/router/index.jsModify: cfc-web/src/views/Layout.vue
[ ] Step 1: 创建 src/views/admin/SysConfig.vue
<template>
<div class="sys-config">
<el-card>
<div slot="header">
<span>系统配置</span>
</div>
<el-table :data="configs" border style="width: 100%">
<el-table-column prop="configKey" label="配置键" min-width="200" />
<el-table-column prop="configValue" label="配置值" min-width="300">
<template slot-scope="scope">
<el-input v-model="scope.row.configValue" :disabled="!isEditing[scope.row.id]" size="small" />
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="300" />
<el-table-column prop="updatedAt" label="更新时间" width="180" />
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button v-if="!isEditing[scope.row.id]" size="mini" @click="startEdit(scope.row)">编辑</el-button>
<template v-else>
<el-button size="mini" type="primary" @click="saveConfig(scope.row)">保存</el-button>
<el-button size="mini" @click="cancelEdit(scope.row)">取消</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { adminConfigList, adminConfigUpdate } from '@/api/product';
export default {
data() {
return {
configs: [],
isEditing: {}
};
},
mounted() {
this.loadConfigs();
},
methods: {
async loadConfigs() {
try {
const res = await adminConfigList({});
if (res.data && res.data.code === 200) {
this.configs = res.data.data || [];
}
} catch (e) {
this.$message.error('加载失败');
}
},
startEdit(row) {
this.$set(this.isEditing, row.id, true);
},
cancelEdit(row) {
this.$set(this.isEditing, row.id, false);
this.loadConfigs();
},
async saveConfig(row) {
try {
const res = await adminConfigUpdate({
configKey: row.configKey,
configValue: row.configValue
});
if (res.data && res.data.code === 200) {
this.$message.success('保存成功');
this.$set(this.isEditing, row.id, false);
this.loadConfigs();
} else {
this.$message.error(res.data.message || '保存失败');
}
} catch (e) {
this.$message.error('保存失败');
}
}
}
};
</script>
<style scoped>
.sys-config {
padding: 20px;
}
</style>
[ ] Step 2: 添加路由
在 cfc-web/src/router/index.js 中添加:
{
path: '/admin/config',
component: () => import('@/views/admin/SysConfig.vue'),
meta: { title: '系统配置', icon: 'setting' }
}
在 cfc-web/src/views/Layout.vue 菜单中添加:
<el-menu-item index="/admin/config">
<i class="el-icon-setting"></i>
<span slot="title">系统配置</span>
</el-menu-item>
[ ] Step 4: 提交
cd /mnt/e/workspace/dan/zxyj && git add cfc-web/src/views/admin/SysConfig.vue cfc-web/src/router/index.js cfc-web/src/views/Layout.vue
git commit -m "feat(phase1): Web 端系统配置页面+路由+菜单"
Files: 无(仅运行验证命令)
[ ] Step 1: 后端编译验证
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn clean compile -q
Expected: mvn: ok(无错误)
[ ] Step 2: 启动后端
# 杀掉旧进程(如有)
pkill -f "cfc-backend" 2>/dev/null || true
sleep 2
# 启动新进程
cd /mnt/e/workspace/dan/zxyj/cfc-backend && rtk mvn spring-boot:run > /tmp/cfc-backend.log 2>&1 &
echo "Started, PID: $!"
# 等待启动完成
sleep 30
rtk grep "Started" /tmp/cfc-backend.log
Expected: 输出包含 "Started CfcApplication"
[ ] Step 3: 测试商品列表 API
curl -s -X POST http://localhost:8080/api/product/list \
-H "Content-Type: application/json" \
-d '{"page":1,"size":10}' | rtk python3 -m json.tool
Expected: 返回 code: 200,data.records 含 5 个种子商品
[ ] Step 4: 测试系统配置 API
curl -s -X POST http://localhost:8080/api/admin/config/list \
-H "Content-Type: application/json" -d '{}' | rtk python3 -m json.tool
Expected: 返回 code: 200,data 含 8 条配置记录
[ ] Step 5: 验证前端文件可解析
# 检查所有新增的 .vue 文件能被读出
for f in \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/shop/index.vue \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/discover/index.vue \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/discover/product-detail.vue \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/shop/order-list.vue \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/shop/order-detail.vue \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/vendor/products.vue \
/mnt/e/workspace/dan/zxyj/cfc-frontend/pages/vendor/orders.vue
do
rtk head -1 "$f" > /dev/null && echo "OK: $f" || echo "FAIL: $f"
done
Expected: 全部输出 "OK: ..."
[ ] Step 6: 验证 pages.json 注册了所有新页面
for p in \
"pages/discover/product-detail" \
"pages/shop/order-list" \
"pages/shop/order-detail" \
"pages/vendor/products" \
"pages/vendor/orders"
do
rtk grep -q "\"$p\"" /mnt/e/workspace/dan/zxyj/cfc-frontend/pages.json && echo "OK: $p" || echo "MISSING: $p"
done
Expected: 全部输出 "OK: ..."
[ ] Step 7: 验证 Web 端路由
for p in "ProductManage" "OrderManage" "SysConfig"; do
rtk grep -q "$p" /mnt/e/workspace/dan/zxyj/cfc-web/src/router/index.js && echo "OK: $p" || echo "MISSING: $p"
done
Expected: 全部输出 "OK: ..."
在 docs/superpowers/plans/2026-06-04-phase1-completion-report.md 写入:
# Phase 1 完成报告
> **日期:** 2026-06-04
> **范围:** 溪福俱乐部 Phase 1 — 商品系统
> **依赖:** Phase 0 角色模型 + TabBar + 服务商入驻
## 交付清单
### 后端 (10 新建 + 2 修改)
- [x] `entity/Product.java` — 商品实体
- [x] `entity/ProductOrder.java` — 订单实体
- [x] `entity/SysConfig.java` — 系统配置实体
- [x] `mapper/ProductMapper.java` / `ProductOrderMapper.java` / `SysConfigMapper.java`
- [x] `dto/ProductDTO.java` / `ProductListQueryDTO.java`
- [x] `dto/ProductOrderDTO.java` / `CreateProductOrderDTO.java`
- [x] `dto/SysConfigDTO.java`
- [x] `service/ProductService.java` — 商品 CRUD/审核/上下架/库存
- [x] `service/ProductOrderService.java` — 订单创建/支付/取消/完成
- [x] `service/SysConfigService.java` — 配置 CRUD
- [x] `controller/product/ProductController.java`
- [x] `controller/product/ProductOrderController.java`
- [x] `controller/config/SysConfigController.java`
- [x] `controller/admin/AdminController.java` — 新增 product/order 审核端点
- [x] `config/DatabaseInitializer.java` — 3 张表 DDL + 商品+配置种子
### 前端 (7 新建 + 1 修改)
- [x] `pages/shop/index.vue` — 商城页(从占位 → 商品列表)
- [x] `pages/discover/index.vue` — 发现页(从占位 → 活动+课程)
- [x] `pages/discover/product-detail.vue` — 商品详情
- [x] `pages/shop/order-list.vue` — 订单列表
- [x] `pages/shop/order-detail.vue` — 订单详情
- [x] `pages/vendor/products.vue` — 服务商商品管理
- [x] `pages/vendor/orders.vue` — 服务商订单管理
- [x] `pages/vendor/center.vue` — 添加商品/订单菜单项
- [x] `utils/api.js` — 新增 19 个商品/订单/配置 API
- [x] `pages.json` — 注册 5 个新页面
### Web 管理端 (3 新建 + 2 修改)
- [x] `api/product.js` — Web API 封装
- [x] `views/admin/ProductManage.vue` — 商品管理
- [x] `views/admin/OrderManage.vue` — 订单管理
- [x] `views/admin/SysConfig.vue` — 系统配置
- [x] `router/index.js` — 3 个新路由
- [x] `views/Layout.vue` — 3 个菜单项
## 测试结果
- [x] 后端编译通过 (`mvn clean compile`)
- [x] 后端启动成功 (Started CfcApplication)
- [x] 商品列表 API 返回 5 条种子数据
- [x] 系统配置 API 返回 8 条配置
- [x] 前端所有新增页面文件可读
- [x] pages.json 注册完整
- [x] Web 端路由注册完整
## 已知限制
- 商品发布功能:Phase 1 仅提供 Web 端/管理端入口;小程序端 `onCreate` 为占位提示(待 Phase 2+ 完善编辑表单)
- 真实微信支付:Phase 1+2 均使用测试模式(直接标记为已支付);Phase 3+ 接入真实支付
- SKU 多规格:未实现(设计 Phase 0-2 暂不支持)
## Phase 2 启动条件
- [x] Product 列表可访问
- [x] 商品已上架(on_shelf)
- [x] 用户可购买(订单流程已通)
- [ ] **Phase 2 计划待编写**
[ ] Step 9: 提交
cd /mnt/e/workspace/dan/zxyj && git add docs/superpowers/plans/2026-06-04-phase1-completion-report.md
git commit -m "docs(phase1): Phase 1 商品系统完成报告"
Phase 1 完成后:
下一步:编写 Phase 2 计划文档(在 Phase 1 验收通过后开始)