Selaa lähdekoodia

feat(shop): 商城重构 — 订单生命周期/SKU/地址/退款/超时取消

- 移除废弃的DanShop集成相关代码
- 调整支付回调路由至商城订单处理
- 新增订单发货(ship)/确认收货(confirmReceive)/取消(cancel)
- 新增配送方式(快递/自提)支持
- 新增ProductSku实体/CRUD/SKU选择器
- 新增UserAddress实体/CRUD
- 新增OrderItem实体/CRUD
- 新增退款申请/审批/拒绝流程
- 新增超时取消自动任务
- 新增库存乐观锁防超卖
- 前端:detail SKU选择器 / checkout skuId传参 / order-list退款操作
- 前端:goShop/goDetail导航修复(address已使用CFC API)
- SQL: 添加product_skus/user_addresses/order_items扩展表

包含关联更新: Activity/Energy模块扩展, 会员试用TrialMembership实体
User 4 kuukautta sitten
vanhempi
sitoutus
fde016ce7d
57 muutettua tiedostoa jossa 1572 lisäystä ja 2764 poistoa
  1. 36 32
      cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java
  2. 0 544
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanshopPaymentController.java
  3. 0 159
      cfc-backend/src/main/java/com/etotem/cfc/controller/InternalApiController.java
  4. 0 44
      cfc-backend/src/main/java/com/etotem/cfc/controller/InternalCommissionController.java
  5. 0 62
      cfc-backend/src/main/java/com/etotem/cfc/controller/InternalEnergyController.java
  6. 35 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductOrderController.java
  7. 0 48
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DanshopMigrationController.java
  8. 63 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ProductSkuController.java
  9. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java
  10. 19 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java
  11. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java
  12. 0 2
      cfc-backend/src/main/java/com/etotem/cfc/dto/LoginResultDTO.java
  13. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/OrderItemDTO.java
  14. 46 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ProductSkuDTO.java
  15. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/UserAddressDTO.java
  16. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java
  17. 0 54
      cfc-backend/src/main/java/com/etotem/cfc/entity/DanshopPaymentRecord.java
  18. 26 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/OrderItem.java
  19. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java
  20. 28 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSku.java
  21. 0 9
      cfc-backend/src/main/java/com/etotem/cfc/mapper/DanshopPaymentRecordMapper.java
  22. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/OrderItemMapper.java
  23. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductSkuMapper.java
  24. 46 2
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java
  25. 0 176
      cfc-backend/src/main/java/com/etotem/cfc/service/DanshopMigrationService.java
  26. 4 107
      cfc-backend/src/main/java/com/etotem/cfc/service/DanshopSyncService.java
  27. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
  28. 10 1
      cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java
  29. 147 30
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  30. 12 7
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
  31. 72 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java
  32. 55 0
      cfc-backend/src/main/java/com/etotem/cfc/task/OrderTimeoutCancelTask.java
  33. 4 5
      cfc-backend/src/main/resources/application.yml
  34. 22 0
      cfc-backend/src/main/resources/mapper/OrderItemMapper.xml
  35. 33 0
      cfc-backend/src/main/resources/mapper/ProductSkuMapper.xml
  36. 33 0
      cfc-backend/src/main/resources/mapper/UserAddressMapper.xml
  37. 85 0
      cfc-backend/src/main/resources/migration_add_shop_tables.sql
  38. 54 0
      cfc-backend/src/main/resources/schema.sql
  39. 0 21
      cfc-frontend/__tests__/api/api.test.js
  40. 0 11
      cfc-frontend/__tests__/config/config.test.js
  41. 205 3
      cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue
  42. 3 3
      cfc-frontend/pages/discover/index.vue
  43. 2 51
      cfc-frontend/pages/discover/product-detail/product-detail.vue
  44. 23 4
      cfc-frontend/pages/shop/checkout/checkout.vue
  45. 90 57
      cfc-frontend/pages/shop/detail/detail.vue
  46. 10 7
      cfc-frontend/pages/shop/order-detail/order-detail.vue
  47. 45 14
      cfc-frontend/pages/shop/order-list/order-list.vue
  48. 171 173
      cfc-frontend/utils/api.js
  49. 0 46
      cfc-web/src/api/danshop.js
  50. 49 0
      cfc-web/src/api/energy.js
  51. 13 31
      cfc-web/src/router/index.js
  52. 3 3
      cfc-web/src/views/Layout.vue
  53. 0 251
      cfc-web/src/views/admin/DanShopCategoryManage.vue
  54. 0 301
      cfc-web/src/views/admin/DanShopLogisticsManage.vue
  55. 0 359
      cfc-web/src/views/admin/DanShopProfitRuleConfig.vue
  56. 18 84
      cfc-web/src/views/admin/OrderManage.vue
  57. 13 61
      cfc-web/src/views/admin/ProductManage.vue

+ 36 - 32
cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java

@@ -3,34 +3,26 @@ package com.etotem.cfc.controller;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ActivityDTO;
 import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.service.ActivityRegistrationService;
 import com.etotem.cfc.service.ActivityService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
-
 import javax.annotation.Resource;
 import java.util.Map;
 
-@Tag(name = "活动管理", description = "五维活动列表和详情")
 @RestController
 @RequestMapping("/api/activity")
 public class ActivityController {
+    @Resource private ActivityService activityService;
+    @Resource private ActivityRegistrationService registrationService;
 
-    @Resource
-    private ActivityService activityService;
-
-    @Operation(summary = "获取活动列表")
     @PostMapping("/list")
-    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
-                                            @RequestAttribute(value = "userId", required = false) Long userId) {
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId) {
         String dimensionCode = (String) params.get("dimensionCode");
-        String activityType = (String) params.get("activityType");
         Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
         Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
         return activityService.list(dimensionCode, page, size, userId);
     }
 
-    @Operation(summary = "获取活动详情")
     @PostMapping("/detail")
     public Result<ActivityDTO> detail(@RequestBody Map<String, Object> params) {
         Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
@@ -38,23 +30,15 @@ public class ActivityController {
         return activityService.detail(id);
     }
 
-    @Operation(summary = "创建活动")
     @PostMapping("/create")
-    public Result<ActivityDTO> create(@RequestBody Activity activity,
-                                       @RequestAttribute(value = "userId", required = false) Long userId) {
-        if (userId != null) {
-            activity.setVendorId(userId);
-        }
+    public Result<ActivityDTO> create(@RequestBody Activity activity, @RequestAttribute(value = "userId", required = false) Long userId) {
+        if (userId != null) activity.setVendorId(userId);
         return activityService.create(activity);
     }
 
-    @Operation(summary = "更新活动")
     @PostMapping("/update")
-    public Result<ActivityDTO> update(@RequestBody Activity activity) {
-        return activityService.update(activity);
-    }
+    public Result<ActivityDTO> update(@RequestBody Activity activity) { return activityService.update(activity); }
 
-    @Operation(summary = "发布活动")
     @PostMapping("/publish")
     public Result<String> publish(@RequestBody Map<String, Object> params) {
         Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
@@ -62,7 +46,6 @@ public class ActivityController {
         return activityService.publish(id);
     }
 
-    @Operation(summary = "结束活动")
     @PostMapping("/end")
     public Result<String> end(@RequestBody Map<String, Object> params) {
         Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
@@ -70,18 +53,39 @@ public class ActivityController {
         return activityService.end(id);
     }
 
-    @Operation(summary = "活动签到")
+    @PostMapping("/register")
+    public Result<Map<String, Object>> register(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId) {
+        Long activityId = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        if (activityId == null) return Result.error("活动ID不能为空");
+        if (childId == null) return Result.error("孩子ID不能为空");
+        return registrationService.register(activityId, userId, childId);
+    }
+
+    @PostMapping("/cancel-registration")
+    public Result<Void> cancelRegistration(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId) {
+        Long activityId = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        if (activityId == null) return Result.error("活动ID不能为空");
+        if (childId == null) return Result.error("孩子ID不能为空");
+        return registrationService.cancel(activityId, userId, childId);
+    }
+
+    @PostMapping("/my-registrations")
+    public Result<Map<String, Object>> myRegistrations(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId) {
+        Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+        return registrationService.getMyRegistrations(userId, childId, page, size);
+    }
+
     @PostMapping("/checkin")
-    public Result<Map<String, Object>> checkin(@RequestBody Map<String, Object> params,
-                                                @RequestAttribute(value = "userId", required = false) Long userId) {
+    public Result<Map<String, Object>> checkin(@RequestBody Map<String, Object> params, @RequestAttribute(value = "userId", required = false) Long userId) {
         Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
         Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
         if (id == null) return Result.error("id不能为空");
-
         Map<String, Object> checkinResult = activityService.checkin(id, userId, childId);
-        if (checkinResult.containsKey("error")) {
-            return Result.error(checkinResult.get("error").toString());
-        }
+        if (checkinResult.containsKey("error")) return Result.error(checkinResult.get("error").toString());
         return Result.success(checkinResult);
     }
-}
+}

+ 0 - 544
cfc-backend/src/main/java/com/etotem/cfc/controller/DanshopPaymentController.java

@@ -1,544 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.DanshopPaymentRecord;
-import com.etotem.cfc.mapper.DanshopPaymentRecordMapper;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.ResponseEntity;
-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 org.springframework.web.client.RestTemplate;
-
-import javax.annotation.Resource;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * Danshop 支付代理控制器
- * <p>
- * 供 danshop 的 CfcPaymentPlugin 调用,代理微信支付流程。
- * CFC 负责与微信支付直接交互,danshop 无需配置微信支付。
- */
-@Slf4j
-@RestController
-@RequestMapping("/api/internal/payment")
-public class DanshopPaymentController {
-
-    @Resource
-    private DanshopPaymentRecordMapper paymentRecordMapper;
-
-    @Value("${wechat.appid}")
-    private String appid;
-
-    @Value("${wechat.mch-id}")
-    private String mchId;
-
-    @Value("${wechat.mch-key}")
-    private String mchKey;
-
-    @Value("${wechat.notify-url}")
-    private String wechatNotifyUrl;
-
-    @Value("${wechat.danshop-notify-url:${wechat.notify-url}}")
-    private String danshopWechatNotifyUrl;
-
-    @Value("${danshop.base-url:http://localhost:8888}")
-    private String danshopBaseUrl;
-
-    private final RestTemplate restTemplate = new RestTemplate();
-
-    /**
-     * 小程序支付(JSAPI)
-     * 由 danshop CfcPaymentPlugin.mpPay() 调用
-     */
-    @PostMapping("/mp-pay")
-    public Result<Map<String, Object>> mpPay(@RequestBody Map<String, Object> params) {
-        try {
-            String orderSn = (String) params.get("orderSn");
-            String orderType = (String) params.getOrDefault("orderType", "ORDER");
-            Integer amount = params.get("amount") instanceof Integer
-                    ? (Integer) params.get("amount")
-                    : Integer.valueOf(params.get("amount").toString());
-            String description = (String) params.getOrDefault("description", "");
-            String openid = (String) params.get("openid");
-
-            if (orderSn == null || amount == null || openid == null) {
-                return Result.error(400, "缺少必要参数: orderSn, amount, openid");
-            }
-
-            // 1. 创建支付记录
-            String outTradeNo = "CFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
-
-            DanshopPaymentRecord record = new DanshopPaymentRecord();
-            record.setOutTradeNo(outTradeNo);
-            record.setDanshopOrderSn(orderSn);
-            record.setDanshopOrderType(orderType);
-            record.setAmount(amount);
-            record.setOpenid(openid);
-            record.setStatus("pending");
-            record.setCreatedAt(new Date());
-            record.setUpdatedAt(new Date());
-            paymentRecordMapper.insert(record);
-
-            // 记录 danshop 回调 URL
-            record.setNotifyUrl(danshopBaseUrl + "/buyer/payment/cashier/notify/CFC");
-            paymentRecordMapper.updateById(record);
-
-            // 2. 调用微信统一下单接口(使用独立的 danshop notify URL)
-            String wxUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
-
-            JSONObject wxParams = new JSONObject();
-            wxParams.put("appid", appid);
-            wxParams.put("mch_id", mchId);
-            wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
-            wxParams.put("body", description.length() > 32 ? description.substring(0, 32) : description);
-            wxParams.put("out_trade_no", outTradeNo);
-            wxParams.put("total_fee", amount);
-            wxParams.put("spbill_create_ip", "127.0.0.1");
-            wxParams.put("notify_url", danshopWechatNotifyUrl);
-            wxParams.put("trade_type", "JSAPI");
-            wxParams.put("openid", openid);
-
-            // 生成签名
-            String sign = generateSign(wxParams, mchKey);
-            wxParams.put("sign", sign);
-
-            log.info("微信统一下单: outTradeNo={}, amount={}, openid={}", outTradeNo, amount, openid);
-
-            ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
-            JSONObject wxResult = JSON.parseObject(response.getBody());
-
-            if ("SUCCESS".equals(wxResult.getString("return_code"))
-                    && "SUCCESS".equals(wxResult.getString("result_code"))) {
-
-                String prepayId = wxResult.getString("prepay_id");
-
-                // 3. 生成前端支付参数
-                Map<String, Object> payParams = new HashMap<>();
-                payParams.put("appId", appid);
-                payParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
-                payParams.put("nonceStr", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
-                payParams.put("package", "prepay_id=" + prepayId);
-                payParams.put("signType", "MD5");
-
-                // 生成 paySign
-                StringBuilder sb = new StringBuilder();
-                sb.append("appId=").append(appid).append("&");
-                sb.append("nonceStr=").append(payParams.get("nonceStr")).append("&");
-                sb.append("package=").append(payParams.get("package")).append("&");
-                sb.append("signType=MD5&");
-                sb.append("timeStamp=").append(payParams.get("timeStamp")).append("&");
-                sb.append("key=").append(mchKey);
-                String paySign = md5Upper(sb.toString());
-                payParams.put("paySign", paySign);
-
-                // 4. 附加信息
-                payParams.put("outTradeNo", outTradeNo);
-                payParams.put("orderSn", orderSn);
-                payParams.put("recordId", record.getId());
-
-                log.info("微信下单成功: prepayId={}", prepayId);
-                return Result.success(payParams);
-            } else {
-                String errMsg = wxResult.getString("return_msg");
-                log.error("微信下单失败: errCode={}, errMsg={}",
-                        wxResult.getString("err_code"), errMsg);
-                return Result.error(500, "微信下单失败: " + errMsg);
-            }
-        } catch (Exception e) {
-            log.error("CFC支付代理异常", e);
-            return Result.error(500, "支付异常: " + e.getMessage());
-        }
-    }
-
-    /**
-     * H5 支付
-     */
-    @PostMapping("/h5-pay")
-    public Result<Map<String, Object>> h5Pay(@RequestBody Map<String, Object> params) {
-        try {
-            String orderSn = (String) params.get("orderSn");
-            Integer amount = params.get("amount") instanceof Integer
-                    ? (Integer) params.get("amount")
-                    : Integer.valueOf(params.get("amount").toString());
-            String description = (String) params.getOrDefault("description", "");
-
-            String outTradeNo = "CFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
-
-            String wxUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
-            JSONObject wxParams = new JSONObject();
-            wxParams.put("appid", appid);
-            wxParams.put("mch_id", mchId);
-            wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
-            wxParams.put("body", description.length() > 32 ? description.substring(0, 32) : description);
-            wxParams.put("out_trade_no", outTradeNo);
-            wxParams.put("total_fee", amount);
-            wxParams.put("spbill_create_ip", "127.0.0.1");
-            wxParams.put("notify_url", wechatNotifyUrl);
-            wxParams.put("trade_type", "MWEB");
-
-            String sign = generateSign(wxParams, mchKey);
-            wxParams.put("sign", sign);
-
-            ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
-            JSONObject wxResult = JSON.parseObject(response.getBody());
-
-            if ("SUCCESS".equals(wxResult.getString("return_code"))
-                    && "SUCCESS".equals(wxResult.getString("result_code"))) {
-
-                Map<String, Object> result = new HashMap<>();
-                result.put("mwebUrl", wxResult.getString("mweb_url"));
-                result.put("outTradeNo", outTradeNo);
-                return Result.success(result);
-            }
-            return Result.error(500, "微信下单失败");
-        } catch (Exception e) {
-            log.error("CFC H5支付异常", e);
-            return Result.error(500, "支付异常: " + e.getMessage());
-        }
-    }
-
-    /**
-     * Native 支付(扫码)
-     */
-    @PostMapping("/native-pay")
-    public Result<Map<String, Object>> nativePay(@RequestBody Map<String, Object> params) {
-        try {
-            String orderSn = (String) params.get("orderSn");
-            Integer amount = params.get("amount") instanceof Integer
-                    ? (Integer) params.get("amount")
-                    : Integer.valueOf(params.get("amount").toString());
-            String description = (String) params.getOrDefault("description", "");
-
-            String outTradeNo = "CFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
-
-            String wxUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
-            JSONObject wxParams = new JSONObject();
-            wxParams.put("appid", appid);
-            wxParams.put("mch_id", mchId);
-            wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
-            wxParams.put("body", description.length() > 32 ? description.substring(0, 32) : description);
-            wxParams.put("out_trade_no", outTradeNo);
-            wxParams.put("total_fee", amount);
-            wxParams.put("spbill_create_ip", "127.0.0.1");
-            wxParams.put("notify_url", wechatNotifyUrl);
-            wxParams.put("trade_type", "NATIVE");
-
-            String sign = generateSign(wxParams, mchKey);
-            wxParams.put("sign", sign);
-
-            ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
-            JSONObject wxResult = JSON.parseObject(response.getBody());
-
-            if ("SUCCESS".equals(wxResult.getString("return_code"))
-                    && "SUCCESS".equals(wxResult.getString("result_code"))) {
-
-                Map<String, Object> result = new HashMap<>();
-                result.put("codeUrl", wxResult.getString("code_url"));
-                result.put("outTradeNo", outTradeNo);
-                return Result.success(result);
-            }
-            return Result.error(500, "微信下单失败");
-        } catch (Exception e) {
-            log.error("CFC Native支付异常", e);
-            return Result.error(500, "支付异常: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 支付成功回调通知(由 cfc 内部 WeChat Pay notify 处理完后调用此方法通知 danshop)
-     * 此接口由 PackagePaymentController.handlePayNotify() 在收到微信通知后调用
-     */
-    @PostMapping("/notify-callback")
-    public Result<Void> handleDanshopNotifyCallback(@RequestBody Map<String, Object> params) {
-        try {
-            String outTradeNo = (String) params.get("outTradeNo");
-            String transactionId = (String) params.get("transactionId");
-            String status = (String) params.getOrDefault("status", "paid");
-
-            if (outTradeNo == null) {
-                return Result.error(400, "缺少 outTradeNo");
-            }
-
-            // 查找支付记录
-            DanshopPaymentRecord record = paymentRecordMapper.selectOne(
-                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DanshopPaymentRecord>()
-                            .eq(DanshopPaymentRecord::getOutTradeNo, outTradeNo)
-            );
-
-            if (record == null) {
-                log.warn("未找到支付记录: outTradeNo={}", outTradeNo);
-                return Result.error(404, "支付记录不存在");
-            }
-
-            // 更新记录状态
-            record.setStatus(status);
-            record.setTransactionId(transactionId);
-            record.setUpdatedAt(new Date());
-            paymentRecordMapper.updateById(record);
-
-            // 通知 danshop
-            if (record.getNotifyUrl() != null) {
-                Map<String, Object> notifyBody = new HashMap<>();
-                notifyBody.put("orderSn", record.getDanshopOrderSn());
-                notifyBody.put("orderType", record.getDanshopOrderType());
-                notifyBody.put("transactionId", transactionId);
-                notifyBody.put("amount", record.getAmount() / 100.0);
-                notifyBody.put("status", status);
-
-                try {
-                    log.info("通知 danshop 支付结果: url={}, body={}", record.getNotifyUrl(), notifyBody);
-                    restTemplate.postForEntity(record.getNotifyUrl(), notifyBody, String.class);
-                } catch (Exception e) {
-                    log.error("通知 danshop 失败: {}", e.getMessage());
-                }
-            }
-
-            return Result.success("ok", null);
-        } catch (Exception e) {
-            log.error("处理 danshop 支付回调异常", e);
-            return Result.error(500, "处理异常: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 微信支付异步通知(WeChat Pay → cfc)
-     * 当用户完成微信支付后,微信服务器回调此接口
-     */
-    @PostMapping("/wechat-notify")
-    public String handleWechatNotify(@RequestBody String xmlData) {
-        log.info("Danshop支付-微信异步通知: xmlData={}", xmlData);
-        try {
-            // 解析 XML
-            Map<String, String> wxParams = parseXml(xmlData);
-
-            String returnCode = wxParams.get("return_code");
-            if (!"SUCCESS".equals(returnCode)) {
-                log.warn("微信通知失败: return_code={}", returnCode);
-                return xmlResponse("FAIL", "支付失败");
-            }
-
-            // 验签
-            String sign = wxParams.get("sign");
-            wxParams.remove("sign");
-            String calculatedSign = generateSignFromMap(wxParams, mchKey);
-            if (!sign.equals(calculatedSign)) {
-                log.error("微信通知签名验证失败");
-                return xmlResponse("FAIL", "签名验证失败");
-            }
-
-            String outTradeNo = wxParams.get("out_trade_no");
-            String transactionId = wxParams.get("transaction_id");
-
-            if (outTradeNo == null) {
-                log.error("微信通知缺少 out_trade_no");
-                return xmlResponse("FAIL", "参数缺失");
-            }
-
-            // 查找支付记录
-            DanshopPaymentRecord record = paymentRecordMapper.selectOne(
-                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DanshopPaymentRecord>()
-                            .eq(DanshopPaymentRecord::getOutTradeNo, outTradeNo)
-            );
-
-            if (record == null) {
-                log.warn("微信通知: 未找到支付记录 outTradeNo={}", outTradeNo);
-                return xmlResponse("FAIL", "记录不存在");
-            }
-
-            // 更新记录
-            record.setStatus("paid");
-            record.setTransactionId(transactionId);
-            record.setUpdatedAt(new Date());
-            paymentRecordMapper.updateById(record);
-
-            log.info("Danshop支付-微信支付成功: outTradeNo={}, transactionId={}, danshopOrderSn={}",
-                    outTradeNo, transactionId, record.getDanshopOrderSn());
-
-            // 通知 danshop
-            if (record.getNotifyUrl() != null) {
-                Map<String, Object> notifyBody = new HashMap<>();
-                notifyBody.put("orderSn", record.getDanshopOrderSn());
-                notifyBody.put("orderType", record.getDanshopOrderType());
-                notifyBody.put("transactionId", transactionId);
-                notifyBody.put("amount", record.getAmount() / 100.0);
-                notifyBody.put("status", "paid");
-
-                try {
-                    log.info("通知 danshop 支付结果: url={}", record.getNotifyUrl());
-                    restTemplate.postForEntity(record.getNotifyUrl(), notifyBody, String.class);
-                } catch (Exception e) {
-                    log.error("通知 danshop 支付结果失败: {}", e.getMessage());
-                }
-            }
-
-            return xmlResponse("SUCCESS", "OK");
-        } catch (Exception e) {
-            log.error("处理 Danshop 支付微信通知异常", e);
-            return xmlResponse("FAIL", "处理异常");
-        }
-    }
-
-    /**
-     * 退款处理
-     * 由 danshop CfcPaymentPlugin.refund() 调用
-     */
-    @PostMapping("/refund")
-    public Result<Void> refund(@RequestBody Map<String, Object> params) {
-        try {
-            String orderSn = (String) params.get("orderSn");
-            Double amount = params.get("amount") instanceof Double
-                    ? (Double) params.get("amount")
-                    : Double.valueOf(params.get("amount").toString());
-            String reason = (String) params.getOrDefault("reason", "");
-            String refundNo = (String) params.getOrDefault("refundNo", "");
-
-            if (orderSn == null || amount == null) {
-                return Result.error(400, "缺少必要参数: orderSn, amount");
-            }
-
-            // 查找支付记录
-            DanshopPaymentRecord record = paymentRecordMapper.selectOne(
-                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DanshopPaymentRecord>()
-                            .eq(DanshopPaymentRecord::getDanshopOrderSn, orderSn)
-                            .orderByDesc(DanshopPaymentRecord::getId)
-                            .last("LIMIT 1")
-            );
-
-            if (record == null) {
-                log.warn("退款时未找到支付记录: orderSn={}", orderSn);
-                return Result.error(404, "未找到支付记录");
-            }
-
-            String outRefundNo = "RFC" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 4).toUpperCase();
-
-            // 调用微信退款 API
-            String wxUrl = "https://api.mch.weixin.qq.com/secapi/pay/refund";
-
-            JSONObject wxParams = new JSONObject();
-            wxParams.put("appid", appid);
-            wxParams.put("mch_id", mchId);
-            wxParams.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
-            wxParams.put("out_trade_no", record.getOutTradeNo());
-            wxParams.put("out_refund_no", outRefundNo);
-            wxParams.put("total_fee", record.getAmount());
-            wxParams.put("refund_fee", (int) (amount * 100));
-            wxParams.put("refund_desc", reason);
-
-            String sign = generateSign(wxParams, mchKey);
-            wxParams.put("sign", sign);
-
-            log.info("微信退款请求: outTradeNo={}, refundFee={}", record.getOutTradeNo(), amount * 100);
-
-            // 退款需要双向证书,这里使用 RestTemplate(生产环境需配置证书)
-            // 注意:微信退款 API 需要双向证书,目前简化处理
-            // TODO: 生产环境配置微信支付证书
-            try {
-                ResponseEntity<String> response = restTemplate.postForEntity(wxUrl, wxParams.toJSONString(), String.class);
-                JSONObject wxResult = JSON.parseObject(response.getBody());
-
-                if ("SUCCESS".equals(wxResult.getString("return_code"))
-                        && "SUCCESS".equals(wxResult.getString("result_code"))) {
-                    record.setStatus("refunded");
-                    record.setRefundNo(outRefundNo);
-                    record.setUpdatedAt(new Date());
-                    paymentRecordMapper.updateById(record);
-
-                    log.info("微信退款成功: outRefundNo={}", outRefundNo);
-                    return Result.success("ok", null);
-                } else {
-                    log.error("微信退款失败: {}", wxResult.getString("return_msg"));
-                    return Result.error(500, "退款失败: " + wxResult.getString("return_msg"));
-                }
-            } catch (Exception e) {
-                log.error("微信退款 API 调用失败(可能未配置证书): {}", e.getMessage());
-                // 即使微信退款失败,也记录退款状态
-                record.setStatus("refunded");
-                record.setRefundNo(outRefundNo);
-                record.setUpdatedAt(new Date());
-                paymentRecordMapper.updateById(record);
-                return Result.success("ok, but wechat refund may need certificate config", null);
-            }
-        } catch (Exception e) {
-            log.error("CFC退款处理异常", e);
-            return Result.error(500, "退款异常: " + e.getMessage());
-        }
-    }
-
-    // ==================== 工具方法 ====================
-
-    private String generateSign(JSONObject params, String key) {
-        StringBuilder sb = new StringBuilder();
-        params.keySet().stream().sorted().forEach(k -> {
-            if (params.getString(k) != null && !params.getString(k).isEmpty()) {
-                sb.append(k).append("=").append(params.getString(k)).append("&");
-            }
-        });
-        sb.append("key=").append(key);
-        return md5Upper(sb.toString());
-    }
-
-    private String generateSignFromMap(Map<String, String> params, String key) {
-        StringBuilder sb = new StringBuilder();
-        params.keySet().stream().sorted().forEach(k -> {
-            String v = params.get(k);
-            if (v != null && !v.isEmpty()) {
-                sb.append(k).append("=").append(v).append("&");
-            }
-        });
-        sb.append("key=").append(key);
-        return md5Upper(sb.toString());
-    }
-
-    /**
-     * 简单 XML 解析为 Map
-     */
-    private Map<String, String> parseXml(String xml) {
-        Map<String, String> map = new HashMap<>();
-        try {
-            javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance();
-            javax.xml.parsers.DocumentBuilder builder = factory.newDocumentBuilder();
-            org.w3c.dom.Document doc = builder.parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
-            org.w3c.dom.Element root = doc.getDocumentElement();
-            org.w3c.dom.NodeList nodeList = root.getChildNodes();
-            for (int i = 0; i < nodeList.getLength(); i++) {
-                org.w3c.dom.Node node = nodeList.item(i);
-                if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
-                    map.put(node.getNodeName(), node.getTextContent());
-                }
-            }
-        } catch (Exception e) {
-            log.error("XML解析失败", e);
-        }
-        return map;
-    }
-
-    /**
-     * 构建微信 XML 响应
-     */
-    private String xmlResponse(String returnCode, String returnMsg) {
-        return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code>"
-                + "<return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
-    }
-
-    private String md5Upper(String str) {
-        try {
-            java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
-            byte[] digest = md.digest(str.getBytes("UTF-8"));
-            StringBuilder hex = new StringBuilder();
-            for (byte b : digest) {
-                hex.append(String.format("%02x", b));
-            }
-            return hex.toString().toUpperCase();
-        } catch (Exception e) {
-            throw new RuntimeException("MD5失败", e);
-        }
-    }
-}

+ 0 - 159
cfc-backend/src/main/java/com/etotem/cfc/controller/InternalApiController.java

@@ -1,159 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.UserMapper;
-import com.etotem.cfc.service.PointsService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * DanShop 内部调用接口
- * 供 DanShop 独立模块 (port 9081) 调用
- * 所有接口仅供内部服务间调用,请勿暴露到公网
- */
-@Slf4j
-@RestController
-@RequestMapping("/api/internal")
-public class InternalApiController {
-
-    @Resource
-    private UserMapper userMapper;
-
-    @Resource
-    private JdbcTemplate jdbcTemplate;
-
-    /**
-     * 验证 JWT Token,获取用户ID
-     * DanShop 内部调用: POST /api/auth/verify
-     * 返回格式: {"code":0,"data":{"userId":123}}
-     */
-    @PostMapping("/auth/verify")
-    public Result<Map<String, Long>> verifyToken(@RequestHeader(value = "Authorization", required = false) String authHeader) {
-        try {
-            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
-                return Result.error(401, "Missing or invalid Authorization header");
-            }
-            String token = authHeader.substring(7);
-            // 简单token验证: token格式为 userId_timestamp_signature
-            // 这里直接解析token获取userId(实际项目中应使用JWT验证)
-            Long userId = parseUserIdFromToken(token);
-            if (userId == null) {
-                return Result.error(401, "Invalid token");
-            }
-            Map<String, Long> result = new HashMap<>();
-            result.put("userId", userId);
-            return Result.success(result);
-        } catch (Exception e) {
-            log.warn("Token验证失败: {}", e.getMessage());
-            return Result.error(401, "Token verification failed: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 发放能量值给用户
-     * DanShop 内部调用: POST /api/energy/grant
-     */
-    @PostMapping("/energy/grant")
-    public Result<Void> grantEnergy(@RequestBody Map<String, Object> params) {
-        try {
-            Long userId = getLong(params, "userId");
-            Integer amount = getInt(params, "amount");
-            String reason = (String) params.getOrDefault("reason", "DanShop购物奖励");
-
-            if (userId == null || amount == null) {
-                return Result.error(400, "userId and amount are required");
-            }
-
-            // 记录能量发放(调用积分服务)
-            // 能量值存储在 children 表的 energy_points 或独立的 energy_balance 表
-            // 这里直接写入 energy_log 表
-            try {
-                jdbcTemplate.execute(
-                    "INSERT INTO energy_log (child_id, dimension_id, amount, balance_after, source_type, source_id, description) " +
-                    "SELECT c.id, 0, " + amount + ", 0, 'product', 0, '" + reason + "' " +
-                    "FROM children c WHERE c.user_id = " + userId + " LIMIT 1"
-                );
-            } catch (Exception e) {
-                log.warn("发放能量失败(可能用户没有关联孩子): {}", e.getMessage());
-            }
-
-            return Result.success("ok", null);
-        } catch (Exception e) {
-            log.error("发放能量异常: {}", e.getMessage(), e);
-            return Result.error(500, "Internal error: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 查询用户层级关系
-     * DanShop 内部调用: GET /api/user/{userId}/relation
-     */
-    @GetMapping("/user/{userId}/relation")
-    public Result<Map<String, Object>> getUserRelation(@PathVariable Long userId) {
-        try {
-            if (userId == null) {
-                return Result.error(400, "userId is required");
-            }
-            User user = userMapper.selectById(userId);
-            if (user == null) {
-                return Result.error(404, "User not found");
-            }
-            Map<String, Object> relation = new HashMap<>();
-            relation.put("userId", user.getId());
-            relation.put("referrerId", user.getReferrerId());
-            relation.put("referralCode", user.getReferralCode());
-            relation.put("role", user.getRole());
-            relation.put("familyId", user.getFamilyId());
-            return Result.success(relation);
-        } catch (Exception e) {
-            log.error("查询用户关系异常: {}", e.getMessage(), e);
-            return Result.error(500, "Internal error: " + e.getMessage());
-        }
-    }
-
-    // ==================== 工具方法 ====================
-
-    private Long parseUserIdFromToken(String token) {
-        try {
-            // token格式: userId_timestamp_md5(userId+timestamp+secret)
-            // 这里简化处理,直接取第一段作为userId(演示用)
-            if (token.contains("_")) {
-                String[] parts = token.split("_");
-                return Long.parseLong(parts[0]);
-            }
-            // 尝试直接解析为long
-            return Long.parseLong(token);
-        } catch (NumberFormatException e) {
-            log.warn("无法解析token: {}", token);
-            return null;
-        }
-    }
-
-    private Long getLong(Map<String, Object> map, String key) {
-        Object value = map.get(key);
-        if (value == null) return null;
-        if (value instanceof Long) return (Long) value;
-        if (value instanceof Integer) return ((Integer) value).longValue();
-        if (value instanceof String) {
-            try { return Long.parseLong((String) value); } catch (Exception e) { return null; }
-        }
-        return null;
-    }
-
-    private Integer getInt(Map<String, Object> map, String key) {
-        Object value = map.get(key);
-        if (value == null) return null;
-        if (value instanceof Integer) return (Integer) value;
-        if (value instanceof Long) return ((Long) value).intValue();
-        if (value instanceof String) {
-            try { return Integer.parseInt((String) value); } catch (Exception e) { return null; }
-        }
-        return null;
-    }
-}

+ 0 - 44
cfc-backend/src/main/java/com/etotem/cfc/controller/InternalCommissionController.java

@@ -1,44 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.CommissionSettleRequest;
-import com.etotem.cfc.service.CommissionService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-
-/**
- * DanShop 内部佣金结算接口
- * 供 DanShop 独立模块 (port 9081) 回调
- * 所有接口仅供内部服务间调用,请勿暴露到公网
- */
-@Slf4j
-@RestController
-@RequestMapping("/api/internal/commission")
-public class InternalCommissionController {
-
-    @Resource
-    private CommissionService commissionService;
-
-    @PostMapping("/settle")
-    public Result<Void> settle(@RequestBody CommissionSettleRequest req) {
-        try {
-            if (req.getOrderId() == null || req.getBuyerId() == null || req.getOrderAmount() == null) {
-                return Result.error(400, "orderId, buyerId and orderAmount are required");
-            }
-            commissionService.settle(
-                    req.getOrderId(),
-                    req.getOrderType() != null ? req.getOrderType() : "danshop",
-                    req.getBuyerId(),
-                    req.getOrderAmount(),
-                    req.getProductId()
-            );
-            log.info("佣金结算完成: orderId={}, buyerId={}", req.getOrderId(), req.getBuyerId());
-            return Result.success("ok", null);
-        } catch (Exception e) {
-            log.error("佣金结算异常: orderId={}", req.getOrderId(), e);
-            return Result.error(500, "Commission settle failed: " + e.getMessage());
-        }
-    }
-}

+ 0 - 62
cfc-backend/src/main/java/com/etotem/cfc/controller/InternalEnergyController.java

@@ -1,62 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.EnergyAwardRequest;
-import com.etotem.cfc.entity.Child;
-import com.etotem.cfc.mapper.ChildMapper;
-import com.etotem.cfc.service.EnergyService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.Map;
-
-/**
- * DanShop 内部能量发放接口
- * 供 DanShop 独立模块 (port 9081) 回调
- * 安全边界: 所有接口仅供内部服务间调用,请勿暴露到公网
- */
-@Slf4j
-@RestController
-@RequestMapping("/api/internal/energy")
-public class InternalEnergyController {
-
-    @Resource
-    private EnergyService energyService;
-
-    @Resource
-    private ChildMapper childMapper;
-
-    @PostMapping("/award")
-    public Result<Map<String, Integer>> award(@RequestBody EnergyAwardRequest req) {
-        try {
-            if (req.getUserId() == null || req.getAmount() == null) {
-                return Result.error(400, "userId and amount are required");
-            }
-
-            Child child = childMapper.selectOne(
-                    new LambdaQueryWrapper<Child>().eq(Child::getUserId, req.getUserId()).last("LIMIT 1")
-            );
-            if (child == null) {
-                log.warn("能量发放跳过: userId={} 无关联孩子记录", req.getUserId());
-                return Result.error(404, "No child found for userId: " + req.getUserId());
-            }
-
-            Map<String, Integer> result = energyService.awardEnergy(
-                    child.getId(),
-                    "product",
-                    0L,
-                    req.getAmount(),
-                    req.getReason() != null ? req.getReason() : "DanShop购物奖励",
-                    null
-            );
-
-            log.info("能量发放完成: userId={}, childId={}, amount={}", req.getUserId(), child.getId(), req.getAmount());
-            return Result.success(result);
-        } catch (Exception e) {
-            log.error("能量发放异常: userId={}", req.getUserId(), e);
-            return Result.error(500, "Energy award failed: " + e.getMessage());
-        }
-    }
-}

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductOrderController.java

@@ -0,0 +1,35 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ProductOrderService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+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.Map;
+
+@RestController
+@RequestMapping("/api/admin/product/order")
+public class AdminProductOrderController {
+
+    @Resource
+    private ProductOrderService orderService;
+
+    @PostMapping("/refund/approve")
+    public Result<String> approveRefund(@RequestBody Map<String, Object> params,
+                                        @RequestAttribute("userId") Long adminId) {
+        String orderNo = (String) params.get("orderNo");
+        return orderService.approveRefund(orderNo, adminId);
+    }
+
+    @PostMapping("/refund/reject")
+    public Result<String> rejectRefund(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("userId") Long adminId) {
+        String orderNo = (String) params.get("orderNo");
+        String reason = (String) params.get("reason");
+        return orderService.rejectRefund(orderNo, adminId, reason);
+    }
+}

+ 0 - 48
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DanshopMigrationController.java

@@ -1,48 +0,0 @@
-package com.etotem.cfc.controller.admin;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.service.DanshopMigrationService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.annotation.Resource;
-import java.util.Map;
-
-/**
- * DanShop 批量数据迁移控制器
- * <p>
- * 将 CFC 现有数据(用户、商品等)批量同步到 DanShop。
- * 首次集成后执行一次全量迁移,之后增量数据通过业务操作自动同步。
- */
-@Tag(name = "DanShop迁移", description = "CFC → DanShop 数据迁移")
-@RestController
-@RequestMapping("/api/admin/danshop-migrate")
-public class DanshopMigrationController {
-
-    @Resource
-    private DanshopMigrationService danshopMigrationService;
-
-    @Operation(summary = "批量迁移用户到 DanShop")
-    @PostMapping("/users")
-    public Result<Map<String, Object>> migrateUsers() {
-        Map<String, Object> result = danshopMigrationService.migrateUsers();
-        return Result.success(result);
-    }
-
-    @Operation(summary = "批量迁移商品到 DanShop")
-    @PostMapping("/products")
-    public Result<Map<String, Object>> migrateProducts() {
-        Map<String, Object> result = danshopMigrationService.migrateProducts();
-        return Result.success(result);
-    }
-
-    @Operation(summary = "全量迁移(用户 + 商品)")
-    @PostMapping("/all")
-    public Result<Map<String, Object>> migrateAll() {
-        Map<String, Object> result = danshopMigrationService.migrateAll();
-        return Result.success(result);
-    }
-}

+ 63 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ProductSkuController.java

@@ -0,0 +1,63 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ProductSkuDTO;
+import com.etotem.cfc.entity.ProductSku;
+import com.etotem.cfc.service.ProductSkuService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+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;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api/admin/product/sku")
+public class ProductSkuController {
+
+    @Resource
+    private ProductSkuService skuService;
+
+    @PostMapping("/list")
+    public Result<List<ProductSkuDTO>> listByProduct(@RequestBody Map<String, Object> params) {
+        Long productId = Long.parseLong(params.get("productId").toString());
+        List<ProductSku> skus = skuService.listByProductId(productId);
+        List<ProductSkuDTO> dtos = skus.stream()
+            .map(ProductSkuDTO::from)
+            .collect(Collectors.toList());
+        return Result.success(dtos);
+    }
+
+    @PostMapping("/get")
+    public Result<ProductSkuDTO> get(@RequestBody Map<String, Object> params) {
+        Long id = Long.parseLong(params.get("id").toString());
+        ProductSku sku = skuService.getById(id);
+        if (sku == null) {
+            return Result.error("SKU不存在");
+        }
+        return Result.success(ProductSkuDTO.from(sku));
+    }
+
+    @PostMapping("/create")
+    public Result<String> create(@RequestBody ProductSku sku) {
+        skuService.create(sku);
+        return Result.success("创建成功");
+    }
+
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody ProductSku sku) {
+        skuService.update(sku);
+        return Result.success("更新成功");
+    }
+
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> params) {
+        Long id = Long.parseLong(params.get("id").toString());
+        skuService.delete(id);
+        return Result.success("删除成功");
+    }
+}

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java

@@ -190,7 +190,7 @@ public class AuthController {
     }
 
     /**
-     * JWT Token 验证(供 danshop 等内部服务调用)
+     * JWT Token 验证
      * 从 Authorization header 提取 Bearer token,验证并返回用户信息
      */
     @Operation(summary = "验证JWT Token有效性")

+ 19 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -75,7 +75,25 @@ public class ProductOrderController {
     public Result<String> confirm(@RequestBody Map<String, Object> params,
                                  @RequestAttribute("userId") Long userId) {
         String orderNo = (String) params.get("orderNo");
-        return orderService.confirm(orderNo, userId);
+        return orderService.confirmReceive(orderNo, userId);
+    }
+
+    @PostMapping("/ship")
+    public Result<String> ship(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        String trackingNumber = (String) params.get("trackingNumber");
+        String expressCompany = (String) params.get("expressCompany");
+        return orderService.ship(orderNo, trackingNumber, expressCompany);
+    }
+
+    // ========== 退款流程 ==========
+
+    @PostMapping("/refund/apply")
+    public Result<String> applyRefund(@RequestBody Map<String, Object> params,
+                                      @RequestAttribute("userId") Long userId) {
+        String orderNo = (String) params.get("orderNo");
+        String reason = (String) params.get("reason");
+        return orderService.applyRefund(orderNo, userId, reason);
     }
 
     @PostMapping("/notify")

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java

@@ -14,4 +14,5 @@ public class CreateProductOrderDTO {
     private String addressSnapshot;
     private Integer discountAmount;
     private Long couponId;
+    private Integer deliveryMethod = 1; // 1=快递 2=自提
 }

+ 0 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/LoginResultDTO.java

@@ -17,6 +17,4 @@ public class LoginResultDTO {
     private String teacherStatus; // 成长规划师审核状态: pending/approved/rejected
     private String teacherRejectReason; // 成长规划师审核拒绝原因
     private Boolean hasTeacherRole; // 用户是否拥有teacher角色
-    private String danshopAccessToken; // danshop 商城访问token
-    private String danshopRefreshToken; // danshop 商城刷新token
 }

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/OrderItemDTO.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class OrderItemDTO {
+    private Long id;
+    private Long orderId;
+    private Long productId;
+    private Long skuId;
+    private String productName;
+    private String productImage;
+    private String skuSpecs;
+    private BigDecimal price;
+    private Integer quantity;
+    private BigDecimal subtotal;
+}

+ 46 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductSkuDTO.java

@@ -0,0 +1,46 @@
+package com.etotem.cfc.dto;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.etotem.cfc.entity.ProductSku;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class ProductSkuDTO {
+    private Long id;
+    private Long productId;
+    private String skuCode;
+    private List<Map<String, String>> specs;
+    private BigDecimal price;
+    private BigDecimal originalPrice;
+    private Integer stock;
+    private String image;
+    private Boolean enabled;
+
+    public static ProductSkuDTO from(ProductSku sku) {
+        if (sku == null) return null;
+        ProductSkuDTO dto = new ProductSkuDTO();
+        dto.setId(sku.getId());
+        dto.setProductId(sku.getProductId());
+        dto.setSkuCode(sku.getSkuCode());
+        dto.setPrice(sku.getPrice());
+        dto.setOriginalPrice(sku.getOriginalPrice());
+        dto.setStock(sku.getStock());
+        dto.setImage(sku.getImage());
+        dto.setEnabled(sku.getEnabled() != null && sku.getEnabled() == 1);
+        // Parse specs JSON to List<Map>
+        if (sku.getSpecs() != null && !sku.getSpecs().isEmpty()) {
+            try {
+                List<Map<String, String>> specsList = JSON.parseObject(sku.getSpecs(),
+                    new TypeReference<List<Map<String, String>>>() {});
+                dto.setSpecs(specsList);
+            } catch (Exception e) {
+                dto.setSpecs(null);
+            }
+        }
+        return dto;
+    }
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UserAddressDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class UserAddressDTO {
+    private Long id;
+    private Long userId;
+    private String consignee;
+    private String phone;
+    private String province;
+    private String city;
+    private String district;
+    private String street;
+    private String detail;
+    private Boolean isDefault;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -45,6 +45,15 @@ public class Activity implements Serializable {
     /** 签到积分(0=使用系统默认) */
     private Integer checkinPoints;
 
+    /** 是否需要报名(0=否,1=是) */
+    private Integer requireRegistration;
+
+    /** 是否需要审核签到(0=否,1=是) */
+    private Integer requireCheckinReview;
+
+    /** 未报名签到扣减能量值(正数) */
+    private Integer energyPenaltyUnregistered;
+
     /** public(公开)/restricted(私密)/all(默认)/member */
     private String visibility;
 

+ 0 - 54
cfc-backend/src/main/java/com/etotem/cfc/entity/DanshopPaymentRecord.java

@@ -1,54 +0,0 @@
-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;
-
-/**
- * Danshop 支付代理记录
- * 记录由 danshop 委托 cfc 处理的支付请求
- */
-@Data
-@TableName("danshop_payment_records")
-public class DanshopPaymentRecord implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /** CFC 内部订单号(用作 WeChat Pay out_trade_no) */
-    private String outTradeNo;
-
-    /** Danshop 订单编号 */
-    private String danshopOrderSn;
-
-    /** Danshop 订单类型 (ORDER/TRADE) */
-    private String danshopOrderType;
-
-    /** 金额(分) */
-    private Integer amount;
-
-    /** 用户微信 openid */
-    private String openid;
-
-    /** 支付状态: pending / paid / refunded */
-    private String status;
-
-    /** 微信支付交易号 */
-    private String transactionId;
-
-    /** Danshop 回调通知 URL */
-    private String notifyUrl;
-
-    /** 退款单号 */
-    private String refundNo;
-
-    /** 创建时间 */
-    private Date createdAt;
-
-    /** 更新时间 */
-    private Date updatedAt;
-}

+ 26 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/OrderItem.java

@@ -0,0 +1,26 @@
+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;
+
+@Data
+@TableName("order_items")
+public class OrderItem implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+    private Long productId;
+    private Long skuId;
+    private String productName;
+    private String productImage;
+    private String skuSpecs;
+    private BigDecimal price;
+    private Integer quantity;
+    private BigDecimal subtotal;
+}

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -35,7 +35,17 @@ public class ProductOrder implements Serializable {
     private String addressSnapshot;   // 收货地址快照(JSON)
     private String logisticsNo;       // 物流单号
     private String logisticsCompany;  // 物流公司
+    private String trackingNumber;    // 快递单号
+    private String expressCompany;    // 快递公司
+    private Integer deliveryMethod;   // 配送方式:1快递 2自提
+    private String pickupCode;         // 自提码
+    private Date confirmTime;          // 确认收货时间
     private Date paidAt;
+    // 退款相关字段
+    private Integer refundStatus;      // 退款状态:0无 1申请中 2已退款 3拒绝
+    private String refundReason;       // 退款原因
+    private Integer refundAmount;      // 退款金额(分)
+    private Date refundTime;           // 退款时间
     private Date createdAt;
     private Date updatedAt;
 }

+ 28 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSku.java

@@ -0,0 +1,28 @@
+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_skus")
+public class ProductSku implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+    private String skuCode;
+    private String specs;
+    private BigDecimal price;
+    private Integer stock;
+    private BigDecimal originalPrice;
+    private String image;
+    private Integer enabled;
+    private Date createTime;
+    private Date updateTime;
+}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/DanshopPaymentRecordMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.DanshopPaymentRecord;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface DanshopPaymentRecordMapper extends BaseMapper<DanshopPaymentRecord> {
-}

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/OrderItemMapper.java

@@ -0,0 +1,11 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.OrderItem;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+public interface OrderItemMapper extends BaseMapper<OrderItem> {
+
+    List<OrderItem> findByOrderId(@Param("orderId") Long orderId);
+}

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductSkuMapper.java

@@ -0,0 +1,15 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ProductSku;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+public interface ProductSkuMapper extends BaseMapper<ProductSku> {
+
+    List<ProductSku> findByProductId(@Param("productId") Long productId);
+
+    ProductSku findById(@Param("id") Long id);
+
+    int updateStock(@Param("id") Long id, @Param("quantity") Integer quantity);
+}

+ 46 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ActivityDTO;
 import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.entity.ActivityRegistration;
 import com.etotem.cfc.entity.Child;
 import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.Task;
@@ -20,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
 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;
 
@@ -42,6 +44,9 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     @Resource
     private SysConfigService sysConfigService;
 
+    @Resource
+    private ActivityRegistrationService registrationService;
+
     public Result<Map<String, Object>> list(String dimensionCode, Integer page, Integer size, Long userId) {
         LambdaQueryWrapper<Activity> query = new LambdaQueryWrapper<Activity>()
                 .eq(Activity::getStatus, "published")
@@ -140,7 +145,46 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
             return result;
         }
 
-        // 3. Check if already checked in (dedup by sourceType + sourceId + childId)
+        // 3. Registration check (if activity requires registration)
+        boolean unregistered = false;
+        if (activity.getRequireRegistration() != null && activity.getRequireRegistration() == 1) {
+            boolean isApproved = registrationService.isApproved(activityId, childId);
+            boolean isRegistered = registrationService.isRegistered(activityId, childId);
+            if (!isApproved) {
+                if (isRegistered) {
+                    result.put("error", "报名待审核,请等待审核通过");
+                    return result;
+                }
+                // Not registered at all — apply energy penalty, allow checkin anyway
+                unregistered = true;
+                Integer penaltyAmount = activity.getEnergyPenaltyUnregistered();
+                if (penaltyAmount != null && penaltyAmount > 0) {
+                    try {
+                        int deducted = energyService.deductEnergyByCode(
+                            childId,
+                            activity.getDimensionCode(),
+                            penaltyAmount,
+                            "未报名签到活动《" + activity.getTitle() + "》"
+                        );
+                        if (deducted >= 0) {
+                            result.put("energyPenalty", penaltyAmount);
+                            log.info("未报名签到能量扣减: childId={}, activityId={}, penalty={}, dimension={}",
+                                childId, activityId, penaltyAmount, activity.getDimensionCode());
+                        } else {
+                            result.put("energyPenaltyFailed", "余额不足");
+                            log.warn("未报名签到能量扣减失败(余额不足): childId={}, activityId={}",
+                                childId, activityId);
+                        }
+                    } catch (Exception e) {
+                        log.warn("能量扣减异常: childId={}, activityId={}, error={}",
+                            childId, activityId, e.getMessage());
+                        result.put("energyPenaltyFailed", e.getMessage());
+                    }
+                }
+            }
+        }
+
+        // 4. Check if already checked in (dedup by sourceType + sourceId + childId)
         Task existingTask = taskMapper.selectOne(
             new LambdaQueryWrapper<Task>()
                 .eq(Task::getSourceType, "activity")
@@ -155,7 +199,7 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
             return result;
         }
 
-        // 4. Determine points — use activity's checkinPoints or sysconfig default
+        // 5. Determine points — use activity's checkinPoints or sysconfig default
         int points = 5; // default
         if (activity.getCheckinPoints() != null && activity.getCheckinPoints() > 0) {
             points = activity.getCheckinPoints();

+ 0 - 176
cfc-backend/src/main/java/com/etotem/cfc/service/DanshopMigrationService.java

@@ -1,176 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-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 lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Resource;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Danshop 批量数据迁移服务
- * <p>
- * 将 CFC 现有数据(用户、商品等)批量同步到 DanShop。
- * 用于首次集成时的全量迁移。
- */
-@Slf4j
-@Service
-public class DanshopMigrationService {
-
-    @Resource
-    private UserMapper userMapper;
-
-    @Resource
-    private ProductMapper productMapper;
-
-    @Resource
-    private DanshopSyncService danshopSyncService;
-
-    /**
-     * 批量迁移用户到 DanShop
-     *
-     * @return 迁移结果统计
-     */
-    public Map<String, Object> migrateUsers() {
-        List<User> users = userMapper.selectList(
-                new LambdaQueryWrapper<User>()
-                        .in(User::getRole, "parent", "teacher")
-                        .orderByAsc(User::getId)
-        );
-
-        List<Map<String, Object>> results = new ArrayList<>();
-        int success = 0;
-        int failed = 0;
-
-        log.info("开始迁移用户到 DanShop,共 {} 人", users.size());
-
-        for (User user : users) {
-            try {
-                Map<String, String> token = danshopSyncService.syncMember(user);
-                if (token != null) {
-                    success++;
-                    Map<String, Object> successItem = new HashMap<>();
-                    successItem.put("cfcUserId", user.getId());
-                    successItem.put("nickname", user.getNickname());
-                    successItem.put("status", "success");
-                    results.add(successItem);
-                } else {
-                    failed++;
-                    Map<String, Object> failedItem = new HashMap<>();
-                    failedItem.put("cfcUserId", user.getId());
-                    failedItem.put("nickname", user.getNickname());
-                    failedItem.put("status", "failed");
-                    failedItem.put("error", "sync returned null");
-                    results.add(failedItem);
-                }
-            } catch (Exception e) {
-                failed++;
-                log.error("迁移用户失败: id={}, nickname={}", user.getId(), user.getNickname(), e);
-                Map<String, Object> excItem = new HashMap<>();
-                excItem.put("cfcUserId", user.getId());
-                excItem.put("nickname", user.getNickname());
-                excItem.put("status", "failed");
-                excItem.put("error", e.getMessage());
-                results.add(excItem);
-            }
-
-            // 打印进度
-            if ((success + failed) % 50 == 0) {
-                log.info("用户迁移进度: {}/{}", success + failed, users.size());
-            }
-        }
-
-        log.info("用户迁移完成: 成功={}, 失败={}, 总计={}", success, failed, users.size());
-
-        Map<String, Object> userResult = new HashMap<>();
-        userResult.put("total", users.size());
-        userResult.put("success", success);
-        userResult.put("failed", failed);
-        userResult.put("results", results);
-        return userResult;
-    }
-
-    /**
-     * 批量迁移商品到 DanShop
-     *
-     * @return 迁移结果统计
-     */
-    public Map<String, Object> migrateProducts() {
-        List<Product> products = productMapper.selectList(
-                new LambdaQueryWrapper<Product>()
-                        .in(Product::getStatus, "on_shelf", "approved")
-                        .orderByAsc(Product::getId)
-        );
-
-        List<Map<String, Object>> results = new ArrayList<>();
-        int success = 0;
-        int failed = 0;
-
-        log.info("开始迁移商品到 DanShop,共 {} 个", products.size());
-
-        for (Product product : products) {
-            try {
-                Map<String, Object> result = danshopSyncService.syncProduct(product);
-                if (result != null && "success".equals(result.get("syncStatus"))) {
-                    success++;
-                    Map<String, Object> prodSuccess = new HashMap<>();
-                    prodSuccess.put("cfcProductId", product.getId());
-                    prodSuccess.put("name", product.getName());
-                    prodSuccess.put("status", "success");
-                    prodSuccess.put("goodsId", result.get("goodsId"));
-                    results.add(prodSuccess);
-                } else {
-                    failed++;
-                    Map<String, Object> prodFailed = new HashMap<>();
-                    prodFailed.put("cfcProductId", product.getId());
-                    prodFailed.put("name", product.getName());
-                    prodFailed.put("status", "failed");
-                    prodFailed.put("error", result != null ? result.toString() : "sync returned null");
-                    results.add(prodFailed);
-                }
-            } catch (Exception e) {
-                failed++;
-                log.error("迁移商品失败: id={}, name={}", product.getId(), product.getName(), e);
-                Map<String, Object> prodExc = new HashMap<>();
-                prodExc.put("cfcProductId", product.getId());
-                prodExc.put("name", product.getName());
-                prodExc.put("status", "failed");
-                prodExc.put("error", e.getMessage());
-                results.add(prodExc);
-            }
-
-            if ((success + failed) % 50 == 0) {
-                log.info("商品迁移进度: {}/{}", success + failed, products.size());
-            }
-        }
-
-        log.info("商品迁移完成: 成功={}, 失败={}, 总计={}", success, failed, products.size());
-
-        Map<String, Object> prodResult = new HashMap<>();
-        prodResult.put("total", products.size());
-        prodResult.put("success", success);
-        prodResult.put("failed", failed);
-        prodResult.put("results", results);
-        return prodResult;
-    }
-
-    /**
-     * 全量迁移(用户 + 商品)
-     */
-    public Map<String, Object> migrateAll() {
-        Map<String, Object> userResult = migrateUsers();
-        Map<String, Object> productResult = migrateProducts();
-
-        Map<String, Object> allResult = new HashMap<>();
-        allResult.put("users", userResult);
-        allResult.put("products", productResult);
-        return allResult;
-    }
-}

+ 4 - 107
cfc-backend/src/main/java/com/etotem/cfc/service/DanshopSyncService.java

@@ -1,114 +1,11 @@
 package com.etotem.cfc.service;
 
-import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.entity.User;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-import javax.annotation.Resource;
-import java.util.HashMap;
-import java.util.Map;
 
+/**
+ * Stub for danshop sync service.
+ * Danshop is a separate subsystem; this stub exists so cfc-backend compiles independently.
+ */
 @Service
 public class DanshopSyncService {
-
-    @Value("${danshop.api-url}")
-    private String danshopApiUrl;
-
-    @Resource
-    private RestTemplate restTemplate;
-
-    /**
-     * 同步用户到 danshop,返回 danshop token 信息
-     * @return { accessToken, refreshToken } or null if sync fails
-     */
-    public Map<String, String> syncMember(User user) {
-        try {
-            // Build request body
-            Map<String, Object> request = new HashMap<>();
-            request.put("openid", user.getOpenid() != null ? user.getOpenid() : "");
-            request.put("unionid", user.getUnionid() != null ? user.getUnionid() : "");
-            request.put("phone", user.getPhone() != null ? user.getPhone() : "");
-            request.put("nickname", user.getNickname() != null ? user.getNickname() : "");
-            request.put("avatar", user.getAvatar() != null ? user.getAvatar() : "");
-
-            String url = danshopApiUrl + "/member/sync";
-            
-            @SuppressWarnings("unchecked")
-            Map<String, Object> response = restTemplate.postForObject(url, request, Map.class);
-            
-            if (response != null && response.get("code") != null && 
-                (response.get("code").equals(200) || response.get("code").equals(0) || response.get("code").equals("200"))) {
-                Object data = response.get("data");
-                if (data instanceof Map) {
-                    Map<String, Object> tokenData = (Map<String, Object>) data;
-                    Map<String, String> result = new HashMap<>();
-                    result.put("accessToken", tokenData.get("accessToken") != null ? tokenData.get("accessToken").toString() : null);
-                    result.put("refreshToken", tokenData.get("refreshToken") != null ? tokenData.get("refreshToken").toString() : null);
-                    return result;
-                }
-            }
-            return null;
-        } catch (Exception e) {
-            // Don't fail the whole login if danshop sync fails
-            // Log the error
-            System.err.println("Danshop sync failed for user " + user.getId() + ": " + e.getMessage());
-            return null;
-        }
-    }
-
-    /**
-     * 同步商品到 danshop
-     * @return data map if sync succeeded, null otherwise
-     */
-    public Map<String, Object> syncProduct(Long cfcProductId, String name, String description,
-                                             String coverImage, Integer price, Integer stock,
-                                             String productType, Integer memberPrice,
-                                             String status, String vendorName) {
-        try {
-            Map<String, Object> request = new HashMap<>();
-            request.put("cfcProductId", cfcProductId);
-            request.put("name", name != null ? name : "");
-            request.put("description", description != null ? description : "");
-            request.put("coverImage", coverImage != null ? coverImage : "");
-            request.put("price", price != null ? price : 0);
-            request.put("stock", stock != null ? stock : 0);
-            request.put("productType", productType != null ? productType : "");
-            request.put("memberPrice", memberPrice != null ? memberPrice : 0);
-            request.put("status", status != null ? status : "");
-            request.put("vendorName", vendorName != null ? vendorName : "");
-
-            String url = danshopApiUrl + "/product/sync";
-
-            @SuppressWarnings("unchecked")
-            Map<String, Object> response = restTemplate.postForObject(url, request, Map.class);
-
-            if (response != null && response.get("code") != null &&
-                (response.get("code").equals(200) || response.get("code").equals(0))) {
-                return (Map<String, Object>) response.get("data");
-            }
-            return null;
-        } catch (Exception e) {
-            System.err.println("Danshop product sync failed for product " + cfcProductId + ": " + e.getMessage());
-            return null;
-        }
-    }
-
-    /**
-     * 同步 Product 实体到 danshop
-     */
-    public Map<String, Object> syncProduct(Product product) {
-        return syncProduct(
-            product.getId(),
-            product.getName(),
-            product.getDescription(),
-            product.getCoverImage(),
-            product.getPrice(),
-            product.getStock(),
-            product.getProductType(),
-            product.getMemberPrice(),
-            product.getStatus(),
-            product.getVendorName()
-        );
-    }
 }

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -681,6 +681,21 @@ public class EnergyService {
         return balance.getBalance();
     }
 
+    /**
+     * 扣除能量 — 通过维度code(自动查ID)
+     *
+     * @return 扣除后余额,-1 = 余额不足
+     */
+    @Transactional
+    public int deductEnergyByCode(Long childId, String dimensionCode, Integer amount, String reason) {
+        EnergyDimension dim = getDimByCode(dimensionCode);
+        if (dim == null) {
+            log.warn("EnergyService.deductEnergyByCode: dimension not found, code={}", dimensionCode);
+            return -1;
+        }
+        return deductEnergy(childId, dim.getId(), amount, reason);
+    }
+
     /**
      * 查询五维能量概览
      */

+ 10 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.mapper.PackageOrderMapper;
+import com.etotem.cfc.service.ProductOrderService;
 import com.etotem.cfc.service.api.PaymentServiceInterface;
 import okhttp3.MediaType;
 import okhttp3.OkHttpClient;
@@ -52,6 +53,9 @@ public class PaymentService implements PaymentServiceInterface {
     @Resource
     private CommissionService commissionService;
 
+    @Resource
+    private ProductOrderService productOrderService;
+
     @Value("${wechat.appid}")
     private String appid;
 
@@ -210,7 +214,12 @@ public class PaymentService implements PaymentServiceInterface {
 
             log.info("微信支付通知: orderNo={}, transactionId={}", orderNo, transactionId);
 
-            handlePaymentCallback(orderNo, transactionId, "wechat");
+            // Route to appropriate handler based on order prefix
+            if (orderNo != null && orderNo.startsWith("PO")) {
+                productOrderService.handlePaymentSuccess(orderNo, transactionId);
+            } else {
+                handlePaymentCallback(orderNo, transactionId, "wechat");
+            }
 
             Map<String, Object> result = new HashMap<>();
             result.put("code", "SUCCESS");

+ 147 - 30
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -24,10 +24,14 @@ import java.util.stream.Collectors;
 import com.etotem.cfc.dto.ChildInfoDTO;
 import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.UserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Service
 public class ProductOrderService {
 
+    private static final Logger log = LoggerFactory.getLogger(ProductOrderService.class);
+
     @Resource
     private ProductOrderMapper orderMapper;
 
@@ -90,6 +94,11 @@ public class ProductOrderService {
         order.setDiscountAmount(dto.getDiscountAmount());
         order.setCouponId(dto.getCouponId());
         order.setAddressSnapshot(dto.getAddressSnapshot());
+        order.setDeliveryMethod(dto.getDeliveryMethod() != null ? dto.getDeliveryMethod() : 1);
+        // Generate pickup code for self-pickup orders
+        if (order.getDeliveryMethod() != null && order.getDeliveryMethod() == 2) {
+            order.setPickupCode(generatePickupCode());
+        }
         order.setCancelAt(new Date(System.currentTimeMillis() + 30 * 60 * 1000));
         order.setCreatedAt(new Date());
         order.setUpdatedAt(new Date());
@@ -158,39 +167,10 @@ public class ProductOrderService {
         if (!"pending".equals(order.getStatus())) {
             return Result.error("订单状态异常");
         }
-        boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
-        if (!stockOk) {
-            return Result.error("库存不足");
-        }
         Map<String, Object> wechatPayParams = paymentService.createWechatPrepay(
                 orderNo, order.getProductName(), order.getTotalAmount());
-        order.setStatus("paid");
-        order.setPaidAt(new Date());
-        order.setUpdatedAt(new Date());
-        orderMapper.updateById(order);
-        commissionService.settle(order.getId(), "product", order.getBuyerId(),
-                order.getTotalAmount(), order.getProductId());
-        // Award energy for product purchase
-        try {
-            User buyer_user = userMapper.selectById(userId);
-            Long childId = null;
-            if ("child".equals(buyer_user.getRole())) {
-                childId = buyer_user.getId();
-            } else {
-                List<ChildInfoDTO> children = userService.getChildren(userId);
-                if (children != null && !children.isEmpty()) {
-                    childId = children.get(0).getId();
-                }
-            }
-            if (childId != null) {
-                energyService.awardEnergy(childId, "product", order.getId(),
-                    order.getTotalAmount(), "商品购买能量奖励", 30);
-            }
-        } catch (Exception e) {
-            // Do not block payment flow
-        }
+        // Do NOT change status or deduct stock here — WeChat callback confirms payment
         Map<String, Object> result = new HashMap<>();
-        result.put("order", ProductOrderDTO.from(order));
         result.put("wechatPayParams", wechatPayParams);
         return Result.success(result);
     }
@@ -204,6 +184,17 @@ public class ProductOrderService {
         if (!"pending".equals(order.getStatus())) {
             return Result.success("订单状态非待支付,跳过");
         }
+        // Deduct stock (optimistic lock - check return value)
+        boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
+        if (!stockOk) {
+            log.error("库存扣减失败: orderNo={}, productId={}, quantity={}",
+                    orderNo, order.getProductId(), order.getQuantity());
+            // Mark order abnormal - requires manual intervention
+            order.setRemark("库存扣减失败");
+            order.setUpdatedAt(new Date());
+            orderMapper.updateById(order);
+            throw new RuntimeException("库存不足");
+        }
         order.setStatus("paid");
         order.setTransactionId(transactionId);
         order.setPaidAt(new Date());
@@ -248,9 +239,125 @@ public class ProductOrderService {
         order.setStatus("cancelled");
         order.setUpdatedAt(new Date());
         orderMapper.updateById(order);
+        // Restore stock on cancel
+        productService.increaseStock(order.getProductId(), order.getQuantity());
         return Result.success("订单已取消");
     }
 
+    public Result<String> ship(String orderNo, String trackingNumber, String expressCompany) {
+        ProductOrder order = orderMapper.selectOne(
+            new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (!"paid".equals(order.getStatus())) {
+            return Result.error("仅已支付订单可发货");
+        }
+        // For self-pickup orders, skip tracking info
+        if (order.getDeliveryMethod() != null && order.getDeliveryMethod() == 2) {
+            order.setStatus("pending_receipt");
+        } else {
+            order.setLogisticsNo(trackingNumber);
+            order.setLogisticsCompany(expressCompany);
+            order.setStatus("pending_receipt");
+        }
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        return Result.success("已发货");
+    }
+
+    public Result<String> confirmReceive(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_receipt".equals(order.getStatus())) {
+            return Result.error("仅待收货订单可确认收货");
+        }
+        order.setStatus("completed");
+        order.setConfirmTime(new Date());
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        return Result.success("已确认收货");
+    }
+
+    // ========== 退款流程 ==========
+
+    public Result<String> applyRefund(String orderNo, Long userId, String reason) {
+        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 (!"paid".equals(order.getStatus()) && !"pending_shipment".equals(order.getStatus())) {
+            return Result.error("仅已支付或待发货订单可申请退款");
+        }
+        // 检查是否已有退款申请
+        if (order.getRefundStatus() != null && order.getRefundStatus() == 1) {
+            return Result.error("已有退款申请正在处理中");
+        }
+        order.setRefundStatus(1); // 申请中
+        order.setRefundReason(reason);
+        order.setStatus("refunding");
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        return Result.success("退款申请已提交");
+    }
+
+    public Result<String> approveRefund(String orderNo, Long adminId) {
+        ProductOrder order = orderMapper.selectOne(
+            new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (order.getRefundStatus() == null || order.getRefundStatus() != 1) {
+            return Result.error("没有待处理的退款申请");
+        }
+        // 调用微信支付退款
+        try {
+            paymentService.refund(orderNo, order.getTotalAmount(), order.getRefundReason());
+        } catch (Exception e) {
+            log.error("微信退款失败: orderNo={}, error={}", orderNo, e.getMessage());
+            return Result.error("退款失败: " + e.getMessage());
+        }
+        // 更新订单状态
+        order.setRefundStatus(2); // 已退款
+        order.setRefundAmount(order.getTotalAmount());
+        order.setRefundTime(new Date());
+        order.setStatus("refunded");
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        // 恢复库存
+        productService.increaseStock(order.getProductId(), order.getQuantity());
+        log.info("退款完成: orderNo={}, adminId={}", orderNo, adminId);
+        return Result.success("退款已处理");
+    }
+
+    public Result<String> rejectRefund(String orderNo, Long adminId, String reason) {
+        ProductOrder order = orderMapper.selectOne(
+            new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (order.getRefundStatus() == null || order.getRefundStatus() != 1) {
+            return Result.error("没有待处理的退款申请");
+        }
+        order.setRefundStatus(3); // 拒绝
+        order.setStatus("paid"); // 恢复原状态
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        log.info("退款申请被拒绝: orderNo={}, adminId={}, reason={}", orderNo, adminId, reason);
+        return Result.success("已拒绝退款申请");
+    }
+
     public Result<List<ProductOrderDTO>> myOrders(Long buyerId, String status) {
         LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
             .eq(ProductOrder::getBuyerId, buyerId)
@@ -345,4 +452,14 @@ public class ProductOrderService {
     private String generateOrderNo() {
         return "PO" + System.currentTimeMillis();
     }
+
+    private String generatePickupCode() {
+        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+        StringBuilder code = new StringBuilder();
+        for (int i = 0; i < 6; i++) {
+            int idx = (int) (Math.random() * chars.length());
+            code.append(chars.charAt(idx));
+        }
+        return code.toString();
+    }
 }

+ 12 - 7
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -29,9 +29,6 @@ public class ProductService {
     @Resource
     private UserMapper userMapper;
 
-    @Resource
-    private DanshopSyncService danshopSyncService;
-
     public Result<Map<String, Object>> list(ProductListQueryDTO query, Long userId) {
         Page<Product> page = new Page<>(query.getPage(), query.getSize());
         LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
@@ -88,8 +85,6 @@ public class ProductService {
         product.setCreatedAt(new Date());
         product.setUpdatedAt(new Date());
         productMapper.insert(product);
-        // 同步到 danshop
-        danshopSyncService.syncProduct(product);
         return Result.success(ProductDTO.from(product));
     }
 
@@ -104,8 +99,6 @@ public class ProductService {
         product.setStatus("pending");
         product.setUpdatedAt(new Date());
         productMapper.updateById(product);
-        // 同步到 danshop
-        danshopSyncService.syncProduct(product);
         return Result.success(ProductDTO.from(productMapper.selectById(product.getId())));
     }
 
@@ -225,6 +218,18 @@ public class ProductService {
         return rows > 0;
     }
 
+    public void increaseStock(Long productId, Integer quantity) {
+        if (quantity == null || quantity <= 0) {
+            return;
+        }
+        Product product = productMapper.selectById(productId);
+        if (product != null && product.getStock() != null) {
+            product.setStock(product.getStock() + quantity);
+            product.setUpdatedAt(new Date());
+            productMapper.updateById(product);
+        }
+    }
+
     public Result<String> updateImages(Long productId, Long vendorId, List<String> images) {
         Product product = productMapper.selectById(productId);
         if (product == null) return Result.error("商品不存在");

+ 72 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java

@@ -0,0 +1,72 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.ProductSku;
+import com.etotem.cfc.mapper.ProductSkuMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class ProductSkuService {
+
+    @Resource
+    private ProductSkuMapper skuMapper;
+
+    public List<ProductSku> listByProductId(Long productId) {
+        return skuMapper.findByProductId(productId);
+    }
+
+    public ProductSku getById(Long id) {
+        return skuMapper.findById(id);
+    }
+
+    @Transactional
+    public ProductSku create(ProductSku sku) {
+        sku.setCreateTime(new Date());
+        sku.setUpdateTime(new Date());
+        if (sku.getEnabled() == null) {
+            sku.setEnabled(1);
+        }
+        skuMapper.insert(sku);
+        return sku;
+    }
+
+    @Transactional
+    public void update(ProductSku sku) {
+        sku.setUpdateTime(new Date());
+        skuMapper.updateById(sku);
+    }
+
+    @Transactional
+    public void delete(Long id) {
+        skuMapper.deleteById(id);
+    }
+
+    @Transactional
+    public boolean decreaseStock(Long skuId, Integer quantity) {
+        int rows = skuMapper.updateStock(skuId, -quantity);
+        return rows > 0;
+    }
+
+    @Transactional
+    public void increaseStock(Long skuId, Integer quantity) {
+        ProductSku sku = skuMapper.findById(skuId);
+        if (sku != null && sku.getStock() != null) {
+            sku.setStock(sku.getStock() + quantity);
+            sku.setUpdateTime(new Date());
+            skuMapper.updateById(sku);
+        }
+    }
+
+    public boolean hasStock(Long skuId, Integer quantity) {
+        ProductSku sku = skuMapper.findById(skuId);
+        if (sku == null || sku.getStock() == null) {
+            return false;
+        }
+        return sku.getStock() >= quantity;
+    }
+}

+ 55 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/OrderTimeoutCancelTask.java

@@ -0,0 +1,55 @@
+package com.etotem.cfc.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.ProductOrder;
+import com.etotem.cfc.mapper.ProductOrderMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+@Component
+public class OrderTimeoutCancelTask {
+
+    private static final Logger log = LoggerFactory.getLogger(OrderTimeoutCancelTask.class);
+
+    @Resource
+    private ProductOrderMapper orderMapper;
+
+    @Value("${shop.order.payment-timeout-minutes:30}")
+    private int paymentTimeoutMinutes;
+
+    @Scheduled(fixedRate = 60000) // 每分钟执行一次
+    public void cancelTimeoutOrders() {
+        // 计算超时时间点
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.MINUTE, -paymentTimeoutMinutes);
+        Date timeoutTime = cal.getTime();
+
+        // 查找超时未支付的订单
+        LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
+            .eq(ProductOrder::getStatus, "pending")
+            .lt(ProductOrder::getCreatedAt, timeoutTime);
+        List<ProductOrder> timeoutOrders = orderMapper.selectList(wrapper);
+
+        if (timeoutOrders.isEmpty()) {
+            return;
+        }
+
+        // 取消每个超时订单
+        for (ProductOrder order : timeoutOrders) {
+            order.setStatus("cancelled");
+            order.setUpdatedAt(new Date());
+            orderMapper.updateById(order);
+            log.info("订单 {} 超时未支付,已自动取消", order.getOrderNo());
+        }
+
+        log.info("本次共取消 {} 个超时订单", timeoutOrders.size());
+    }
+}

+ 4 - 5
cfc-backend/src/main/resources/application.yml

@@ -72,7 +72,6 @@ wechat:
   mch-serial-no: YOUR_MCH_SERIAL_NO
   private-key-path: /path/to/apiclient_key.pem
   notify-url: http://your-domain.com/api/payment/notify
-  danshop-notify-url: http://localhost:9082/api/internal/payment/wechat-notify
   login-url: https://api.weixin.qq.com/sns/jscode2session
   phone-url: https://api.weixin.qq.com/wxa/business/getuserphonenumber
   test-mode: true  # 测试模式:跳过微信API调用,使用模拟数据
@@ -88,6 +87,10 @@ math:
   verify-mode: dify  # dify | eval; dify=走Dify工作流, eval=服务端计算(兜底)
   workflow-id: ""    # Dify 数学判题工作流 ID(留空则使用 eval 兜底)
 
+shop:
+  order:
+    payment-timeout-minutes: 30  # 订单超时时间(分钟)
+
 logging:
   level:
     com.etotem.cfc: debug
@@ -95,7 +98,3 @@ logging:
 # Admin 登录配置
 admin:
   skip-captcha: true  # 测试环境跳过验证码,方便自动化测试
-
-danshop:
-  base-url: http://localhost:8888
-  api-url: http://localhost:8888/api/internal

+ 22 - 0
cfc-backend/src/main/resources/mapper/OrderItemMapper.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.etotem.cfc.mapper.OrderItemMapper">
+
+    <resultMap id="BaseResultMap" type="com.etotem.cfc.entity.OrderItem">
+        <id column="id" property="id"/>
+        <result column="order_id" property="orderId"/>
+        <result column="product_id" property="productId"/>
+        <result column="sku_id" property="skuId"/>
+        <result column="product_name" property="productName"/>
+        <result column="product_image" property="productImage"/>
+        <result column="sku_specs" property="skuSpecs"/>
+        <result column="price" property="price"/>
+        <result column="quantity" property="quantity"/>
+        <result column="subtotal" property="subtotal"/>
+    </resultMap>
+
+    <select id="findByOrderId" resultMap="BaseResultMap">
+        SELECT * FROM order_items WHERE order_id = #{orderId}
+    </select>
+
+</mapper>

+ 33 - 0
cfc-backend/src/main/resources/mapper/ProductSkuMapper.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.etotem.cfc.mapper.ProductSkuMapper">
+
+    <resultMap id="BaseResultMap" type="com.etotem.cfc.entity.ProductSku">
+        <id column="id" property="id"/>
+        <result column="product_id" property="productId"/>
+        <result column="sku_code" property="skuCode"/>
+        <result column="specs" property="specs"/>
+        <result column="price" property="price"/>
+        <result column="stock" property="stock"/>
+        <result column="original_price" property="originalPrice"/>
+        <result column="image" property="image"/>
+        <result column="enabled" property="enabled"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_time" property="updateTime"/>
+    </resultMap>
+
+    <select id="findByProductId" resultMap="BaseResultMap">
+        SELECT * FROM product_skus WHERE product_id = #{productId} AND enabled = 1 ORDER BY id ASC
+    </select>
+
+    <select id="findById" resultMap="BaseResultMap">
+        SELECT * FROM product_skus WHERE id = #{id}
+    </select>
+
+    <update id="updateStock">
+        UPDATE product_skus
+        SET stock = stock - #{quantity}
+        WHERE id = #{id} AND stock >= #{quantity}
+    </update>
+
+</mapper>

+ 33 - 0
cfc-backend/src/main/resources/mapper/UserAddressMapper.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.etotem.cfc.mapper.UserAddressMapper">
+
+    <resultMap id="BaseResultMap" type="com.etotem.cfc.entity.UserAddress">
+        <id column="id" property="id"/>
+        <result column="user_id" property="userId"/>
+        <result column="consignee" property="consignee"/>
+        <result column="phone" property="phone"/>
+        <result column="province" property="province"/>
+        <result column="city" property="city"/>
+        <result column="district" property="district"/>
+        <result column="street" property="street"/>
+        <result column="detail" property="detail"/>
+        <result column="is_default" property="isDefault"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_time" property="updateTime"/>
+    </resultMap>
+
+    <select id="findByUserId" resultMap="BaseResultMap">
+        SELECT * FROM user_addresses WHERE user_id = #{userId} ORDER BY is_default DESC, id DESC
+    </select>
+
+    <select id="findDefaultByUserId" resultMap="BaseResultMap">
+        SELECT * FROM user_addresses WHERE user_id = #{userId} AND is_default = 1 LIMIT 1
+    </select>
+
+    <update id="setDefault">
+        UPDATE user_addresses SET is_default = 0 WHERE user_id = #{userId};
+        UPDATE user_addresses SET is_default = 1 WHERE id = #{addressId} AND user_id = #{userId};
+    </update>
+
+</mapper>

+ 85 - 0
cfc-backend/src/main/resources/migration_add_shop_tables.sql

@@ -0,0 +1,85 @@
+-- Shop Rebuild: Add product_skus, user_addresses, order_items tables and extend product_orders/products
+-- Database: zxyj @ 192.168.16.251:3306
+-- Run manually: mysql -h 192.168.16.251 -u zxyj -p"zxyj@123" zxyj < migration_add_shop_tables.sql
+
+-- ============================================================
+-- NEW TABLE: product_skus
+-- ============================================================
+CREATE TABLE IF NOT EXISTS `product_skus` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `product_id` int(11) NOT NULL COMMENT '商品ID',
+  `sku_code` varchar(64) NOT NULL COMMENT 'SKU编码',
+  `specs` json NOT NULL COMMENT '规格JSON:如[{"name":"颜色","value":"红色"},{"name":"尺寸","value":"L"}]',
+  `price` decimal(10,2) NOT NULL COMMENT 'SKU价格',
+  `stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存',
+  `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价/划线价',
+  `image` varchar(512) DEFAULT NULL COMMENT 'SKU图片',
+  `enabled` tinyint(1) NOT NULL DEFAULT 1,
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_product_id` (`product_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU';
+
+-- ============================================================
+-- NEW TABLE: user_addresses
+-- ============================================================
+CREATE TABLE IF NOT EXISTS `user_addresses` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL COMMENT '用户ID',
+  `consignee` varchar(64) NOT NULL COMMENT '收货人',
+  `phone` varchar(20) NOT NULL COMMENT '联系电话',
+  `province` varchar(32) NOT NULL COMMENT '省',
+  `city` varchar(32) NOT NULL COMMENT '市',
+  `district` varchar(32) NOT NULL COMMENT '区',
+  `street` varchar(64) DEFAULT NULL COMMENT '街道',
+  `detail` varchar(256) NOT NULL COMMENT '详细地址',
+  `is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认:0否 1是',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收货地址';
+
+-- ============================================================
+-- NEW TABLE: order_items
+-- ============================================================
+CREATE TABLE IF NOT EXISTS `order_items` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `order_id` int(11) NOT NULL COMMENT '订单ID',
+  `product_id` int(11) NOT NULL COMMENT '商品ID',
+  `sku_id` int(11) DEFAULT NULL COMMENT 'SKU ID',
+  `product_name` varchar(256) NOT NULL COMMENT '商品名(快照)',
+  `product_image` varchar(512) DEFAULT NULL COMMENT '商品图(快照)',
+  `sku_specs` json DEFAULT NULL COMMENT 'SKU规格(快照)',
+  `price` decimal(10,2) NOT NULL COMMENT '购买单价',
+  `quantity` int(11) NOT NULL DEFAULT 1 COMMENT '数量',
+  `subtotal` decimal(10,2) NOT NULL COMMENT '小计',
+  PRIMARY KEY (`id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_sku_id` (`sku_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细';
+
+-- ============================================================
+-- ALTER TABLE: product_orders — add new columns
+-- ============================================================
+ALTER TABLE `product_orders` ADD COLUMN `delivery_method` tinyint(1) DEFAULT 1 COMMENT '配送方式:1快递 2自提';
+ALTER TABLE `product_orders` ADD COLUMN `address_snapshot` json DEFAULT NULL COMMENT '收货地址快照';
+ALTER TABLE `product_orders` ADD COLUMN `coupon_id` int(11) DEFAULT NULL COMMENT '使用的优惠券ID';
+ALTER TABLE `product_orders` ADD COLUMN `discount_amount` decimal(10,2) DEFAULT 0 COMMENT '优惠金额';
+ALTER TABLE `product_orders` ADD COLUMN `delivery_fee` decimal(10,2) DEFAULT 0 COMMENT '运费';
+ALTER TABLE `product_orders` ADD COLUMN `tracking_number` varchar(128) DEFAULT NULL COMMENT '快递单号';
+ALTER TABLE `product_orders` ADD COLUMN `express_company` varchar(64) DEFAULT NULL COMMENT '快递公司';
+ALTER TABLE `product_orders` ADD COLUMN `refund_status` tinyint(1) DEFAULT 0 COMMENT '退款状态:0无 1申请中 2已退款 3拒绝';
+ALTER TABLE `product_orders` ADD COLUMN `refund_reason` varchar(512) DEFAULT NULL COMMENT '退款原因';
+ALTER TABLE `product_orders` ADD COLUMN `refund_amount` decimal(10,2) DEFAULT NULL COMMENT '退款金额';
+ALTER TABLE `product_orders` ADD COLUMN `refund_time` datetime DEFAULT NULL COMMENT '退款时间';
+ALTER TABLE `product_orders` ADD COLUMN `pickup_code` varchar(16) DEFAULT NULL COMMENT '自提码';
+ALTER TABLE `product_orders` ADD COLUMN `remark` varchar(512) DEFAULT NULL COMMENT '买家备注';
+
+-- ============================================================
+-- ALTER TABLE: products — add new columns
+-- ============================================================
+ALTER TABLE `products` ADD COLUMN `has_sku` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否启用多SKU:0统一价 1多规格';
+ALTER TABLE `products` ADD COLUMN `sku_spec_names` json DEFAULT NULL COMMENT '规格维度名:如["颜色","尺寸"]';
+ALTER TABLE `products` ADD COLUMN `delivery_methods` tinyint(1) DEFAULT 3 COMMENT '配送方式支持:1仅快递 2仅自提 3都支持';

+ 54 - 0
cfc-backend/src/main/resources/schema.sql

@@ -1550,6 +1550,50 @@ CREATE TABLE IF NOT EXISTS blood_type_config (
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='血型配置表';
 
+-- =============================================
+-- 活动报名 与 能量规则引擎表(v1.4)
+-- =============================================
+
+-- 活动报名表
+CREATE TABLE IF NOT EXISTS activity_registrations (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    activity_id BIGINT NOT NULL COMMENT '活动ID',
+    child_id BIGINT NOT NULL COMMENT '孩子ID',
+    user_id BIGINT NOT NULL COMMENT '报名人用户ID(家长)',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/approved/rejected/cancelled',
+    registered_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报名时间',
+    approved_at DATETIME COMMENT '审核通过时间',
+    approved_by BIGINT COMMENT '审核人用户ID',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    UNIQUE KEY uk_activity_child (activity_id, child_id),
+    INDEX idx_activity_id (activity_id),
+    INDEX idx_child_id (child_id),
+    INDEX idx_user_id (user_id),
+    INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动报名表';
+
+-- 能量规则表(规则引擎)
+CREATE TABLE IF NOT EXISTS energy_rule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    rule_code VARCHAR(64) NOT NULL UNIQUE COMMENT '规则编码(唯一)',
+    rule_name VARCHAR(200) NOT NULL COMMENT '规则名称',
+    event_type VARCHAR(32) NOT NULL COMMENT '事件类型: checkin/register/complete/cancel/overdue',
+    action VARCHAR(16) NOT NULL COMMENT '动作: increase/decrease',
+    dimension_code VARCHAR(20) COMMENT '维度代码: body/mind/wisdom/action/wealth(NULL=五维均衡)',
+    amount INT COMMENT '固定变动量(基点)',
+    amount_formula VARCHAR(255) COMMENT '动态公式(如: balance*0.1)',
+    priority INT DEFAULT 0 COMMENT '优先级(数字越大越优先匹配)',
+    conditions JSON COMMENT '触发条件(JSON表达式)',
+    status TINYINT DEFAULT 1 COMMENT '状态: 1=启用 0=禁用',
+    description VARCHAR(500) COMMENT '规则描述',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_event_type (event_type),
+    INDEX idx_status (status),
+    INDEX idx_priority (priority)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量规则表(规则引擎)';
+
 CREATE TABLE IF NOT EXISTS activities (
     id               BIGINT AUTO_INCREMENT PRIMARY KEY,
     title            VARCHAR(100) NOT NULL COMMENT '活动标题',
@@ -1951,3 +1995,13 @@ INSERT IGNORE INTO indicator_definitions (domain, category, code, name, data_typ
 ('physical', '齿痕', 'teeth_mark', '齿痕程度', 'enum', '["无","轻","中","重"]', 5, 1),
 ('physical', '舌下络脉', 'sublingual_vein', '舌下络脉', 'enum', '["正常","轻","中","重"]', 6, 1),
 ('physical', '体质', 'constitution', '体质辨识', 'enum', '["平和质","气虚质","阳虚质","阴虚质","湿热质","痰湿质","血瘀质","气郁质","特禀质"]', 7, 1);
+
+-- =============================================
+-- 活动报名 & 能量规则引擎 v1.4 - 字段扩展
+-- 通过 DatabaseInitializer.java 在 ALTER 阶段执行
+-- =============================================
+
+-- 活动表扩展字段
+ALTER TABLE activities ADD COLUMN require_registration TINYINT DEFAULT 1 COMMENT '是否需要报名: 1=需要, 0=直接参与';
+ALTER TABLE activities ADD COLUMN require_checkin_review TINYINT DEFAULT 1 COMMENT '打卡是否需要审核: 1=需要, 0=直接通过';
+ALTER TABLE activities ADD COLUMN energy_penalty_unregistered INT DEFAULT 5 COMMENT '未报名打卡的行能量扣减值';

+ 0 - 21
cfc-frontend/__tests__/api/api.test.js

@@ -905,25 +905,4 @@ describe('invite card API', () => {
   })
 })
 
-// --------------- danshop module ---------------
 
-describe('danshop API', () => {
-  test('danshopProductList sends POST', async () => {
-    mockSuccess({ code: 200 })
-    await api.danshopProductList({ page: 1 })
-    const opts = lastRequest()
-    expect(opts.url).toMatch(/\/api\/shop\/product\/list$/)
-  })
-
-  test('danshopCartAdd sends POST with data', async () => {
-    mockSuccess({ code: 200 })
-    await api.danshopCartAdd({ goodsId: 1, quantity: 2 })
-    expect(lastRequest().data.goodsId).toBe(1)
-  })
-
-  test('danshopOrderCreate sends POST', async () => {
-    mockSuccess({ code: 200 })
-    await api.danshopOrderCreate({ goodsIds: [1, 2] })
-    expect(lastRequest().url).toMatch(/\/api\/shop\/order\/create$/)
-  })
-})

+ 0 - 11
cfc-frontend/__tests__/config/config.test.js

@@ -25,7 +25,6 @@ describe('config.js', () => {
     })
     const cfg = require('@/config').default
     expect(cfg.API_BASE_URL).toBe('http://localhost:9082')
-    expect(cfg.DANSHOP_BASE_URL).toBe('http://localhost:9081')
   })
 
   test('uses trial URLs when envVersion is trial', () => {
@@ -34,7 +33,6 @@ describe('config.js', () => {
     })
     const cfg = require('@/config').default
     expect(cfg.API_BASE_URL).toBe('https://cfc.iwintrue.com')
-    expect(cfg.DANSHOP_BASE_URL).toBe('https://dev.iwintrue.com/danshop')
   })
 
   test('uses release URLs when envVersion is release', () => {
@@ -43,7 +41,6 @@ describe('config.js', () => {
     })
     const cfg = require('@/config').default
     expect(cfg.API_BASE_URL).toBe('https://cfc.etotem.com.cn')
-    expect(cfg.DANSHOP_BASE_URL).toBe('https://danshop.etotem.com.cn')
   })
 
   test('uses defaults when getAccountInfoSync throws', () => {
@@ -52,7 +49,6 @@ describe('config.js', () => {
     })
     const cfg = require('@/config').default
     expect(cfg.API_BASE_URL).toBe('http://localhost:9082')
-    expect(cfg.DANSHOP_BASE_URL).toBe('http://localhost:8081')
   })
 
   test('uses defaults for unknown envVersion', () => {
@@ -62,7 +58,6 @@ describe('config.js', () => {
     const cfg = require('@/config').default
     // Falls through the switch without matching, keeps defaults
     expect(cfg.API_BASE_URL).toBe('http://localhost:9082')
-    expect(cfg.DANSHOP_BASE_URL).toBe('http://localhost:8081')
   })
 
   test('api(path) returns full URL joining API_BASE_URL + path', () => {
@@ -70,10 +65,4 @@ describe('config.js', () => {
     const result = cfg.api('/api/test')
     expect(result).toBe(cfg.API_BASE_URL + '/api/test')
   })
-
-  test('danshop(path) returns full DANSHOP URL', () => {
-    const cfg = require('@/config').default
-    const result = cfg.danshop('/goods/list')
-    expect(result).toBe(cfg.DANSHOP_BASE_URL + '/goods/list')
-  })
 })

+ 205 - 3
cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue

@@ -66,6 +66,38 @@
           <text class="bottom-vendor" v-if="activity.vendorName">{{ activity.vendorName }}</text>
         </view>
         <view class="action-btns">
+          <!-- 报名按钮(仅 requireRegistration=1 时显示) -->
+          <button
+            v-if="activity.requireRegistration === 1 && !registrationStatus"
+            class="btn-register"
+            :class="{ 'btn-register-loading': registrationLoading }"
+            :disabled="registrationLoading"
+            @click="onRegister"
+          >
+            <text v-if="registrationLoading">报名中...</text>
+            <text v-else>立即报名</text>
+          </button>
+          
+          <!-- 已报名待审核 -->
+          <button
+            v-if="activity.requireRegistration === 1 && registrationStatus === 'pending'"
+            class="btn-register-done"
+            disabled
+          >
+            <text>报名待审核</text>
+          </button>
+          
+          <!-- 已报名可取消 -->
+          <button
+            v-if="activity.requireRegistration === 1 && registrationStatus === 'approved'"
+            class="btn-cancel"
+            :disabled="registrationLoading"
+            @click="onCancelRegistration"
+          >
+            <text>取消报名</text>
+          </button>
+          
+          <!-- 签到按钮 -->
           <button
             class="btn-checkin"
             :class="{ 'btn-checkin-done': checkinDone, 'btn-checkin-loading': checkinLoading }"
@@ -78,6 +110,15 @@
           </button>
         </view>
       </view>
+      
+      <!-- 能量扣减提示弹窗 -->
+      <modal v-if="showPenaltyModal" class="penalty-modal" :show="showPenaltyModal" @close="showPenaltyModal = false">
+        <view class="penalty-content">
+          <text class="penalty-title">签到成功但已扣能量</text>
+          <text class="penalty-desc">您未报名参加此活动,已被扣除 {{ penaltyAmount }} 点能量值</text>
+          <button class="penalty-btn" @click="showPenaltyModal = false">我知道了</button>
+        </view>
+      </modal>
     </scroll-view>
 
     <!-- 空状态 -->
@@ -88,7 +129,7 @@
 </template>
 
 <script>
-import { getActivityDetail, activityCheckin } from '@/utils/api.js'
+import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations } from '@/utils/api.js'
 
 export default {
   data() {
@@ -100,7 +141,11 @@ export default {
       errorMsg: '',
       checkinLoading: false,
       checkinDone: false,
-      checkinPoints: 0
+      checkinPoints: 0,
+      registrationStatus: null,
+      registrationLoading: false,
+      showPenaltyModal: false,
+      penaltyAmount: 0
     }
   },
   onLoad: function(options) {
@@ -126,6 +171,23 @@ export default {
         self.loading = false
         if (res && res.code === 200 && res.data) {
           self.activity = res.data
+          // Load registration status if logged in and activity requires registration
+          if (self.activity.requireRegistration === 1) {
+            var childId = uni.getStorageSync('currentChildId')
+            if (childId) {
+              getMyActivityRegistrations({ childId: childId, page: 1, size: 50 }).then(function(regRes) {
+                if (regRes && regRes.data && regRes.data.records) {
+                  var records = regRes.data.records
+                  for (var i = 0; i < records.length; i++) {
+                    if (records[i].activityId === self.activity.id) {
+                      self.registrationStatus = records[i].status
+                      break
+                    }
+                  }
+                }
+              }).catch(function() {})
+            }
+          }
         } else {
           self.error = true
           self.errorMsg = (res && res.message) || '加载失败'
@@ -154,6 +216,13 @@ export default {
       activityCheckin({ id: self.activity.id, childId: childId }).then(function(res) {
         self.checkinLoading = false
         if (res && res.code === 200 && res.data) {
+          // 检查是否有能量扣减
+          if (res.data.energyPenalty) {
+            self.penaltyAmount = res.data.energyPenalty
+            self.showPenaltyModal = true
+            self.checkinDone = true
+            return
+          }
           if (res.data.alreadyCompleted) {
             self.checkinDone = true
             uni.showToast({ title: '已签到', icon: 'success' })
@@ -167,12 +236,63 @@ export default {
           }
         } else {
           var msg = (res && res.message) || '签到失败'
-          uni.showToast({ title: msg, icon: 'none' })
+          // 如果是报名待审核,提示用户
+          if (msg.indexOf('报名') !== -1 || msg.indexOf('审核') !== -1) {
+            uni.showModal({ title: '提示', content: msg, showCancel: false })
+          } else {
+            uni.showToast({ title: msg, icon: 'none' })
+          }
         }
       }).catch(function(err) {
         self.checkinLoading = false
         uni.showToast({ title: '网络异常', icon: 'none' })
       })
+    },
+    onRegister: function() {
+      var self = this
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+        return
+      }
+      self.registrationLoading = true
+      registerActivity({ id: self.activity.id, childId: childId }).then(function(res) {
+        self.registrationLoading = false
+        if (res && res.code === 200) {
+          self.registrationStatus = 'pending'
+          uni.showToast({ title: '报名成功,请等待审核', icon: 'success' })
+        } else {
+          uni.showToast({ title: (res && res.message) || '报名失败', icon: 'none' })
+        }
+      }).catch(function(err) {
+        self.registrationLoading = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+    onCancelRegistration: function() {
+      var self = this
+      uni.showModal({
+        title: '提示',
+        content: '确定要取消报名吗?',
+        success: function(res) {
+          if (res.confirm) {
+            var childId = uni.getStorageSync('currentChildId')
+            self.registrationLoading = true
+            cancelActivityRegistration({ id: self.activity.id, childId: childId }).then(function(res) {
+              self.registrationLoading = false
+              if (res && res.code === 200) {
+                self.registrationStatus = null
+                uni.showToast({ title: '已取消报名', icon: 'success' })
+              } else {
+                uni.showToast({ title: (res && res.message) || '取消失败', icon: 'none' })
+              }
+            }).catch(function() {
+              self.registrationLoading = false
+              uni.showToast({ title: '网络异常', icon: 'none' })
+            })
+          }
+        }
+      })
     }
   }
 }
@@ -368,4 +488,86 @@ export default {
 .btn-checkin-loading {
   opacity: 0.7;
 }
+
+/* 报名按钮 */
+.btn-register {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 32rpx;
+  font-size: 26rpx;
+  font-weight: 600;
+  line-height: 1.5;
+  min-width: 140rpx;
+  text-align: center;
+}
+.btn-register:active { opacity: 0.85; }
+.btn-register-loading { opacity: 0.7; }
+.btn-register-done {
+  background: #F5F5F5;
+  color: #999;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 32rpx;
+  font-size: 26rpx;
+  line-height: 1.5;
+  min-width: 140rpx;
+  text-align: center;
+}
+.btn-cancel {
+  background: #fff;
+  color: #999;
+  border: 2rpx solid #ddd;
+  border-radius: 40rpx;
+  padding: 14rpx 28rpx;
+  font-size: 24rpx;
+  line-height: 1.5;
+  min-width: 120rpx;
+  text-align: center;
+}
+.btn-cancel:active { opacity: 0.7; }
+
+/* 能量扣减弹窗 */
+.penalty-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+.penalty-content {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 48rpx 40rpx;
+  width: 560rpx;
+  text-align: center;
+}
+.penalty-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #EF4444;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.penalty-desc {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 40rpx;
+  line-height: 1.6;
+}
+.penalty-btn {
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 60rpx;
+  font-size: 28rpx;
+}
 </style>

+ 3 - 3
cfc-frontend/pages/discover/index.vue

@@ -169,12 +169,12 @@ export default {
       this.loadProducts()
     },
     goShop() {
-      uni.navigateTo({ url: '/pages/action/index' })
+      uni.switchTab({ url: '/pages/shop/index' })
     },
     goDetail(id) {
-      const token = uni.getStorageSync('token')
+      var token = uni.getStorageSync('token')
       if (token) {
-        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
+        uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id })
       } else {
         uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/discover/index') })
       }

+ 2 - 51
cfc-frontend/pages/discover/product-detail/product-detail.vue

@@ -44,12 +44,6 @@
           <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="action-btns">
-          <view v-if="danshopProductId" class="action-extra">
-            <view class="btn-favorite" @click="onToggleFavorite">
-              <text :class="isFavorite ? 'icon-heart-active' : 'icon-heart'">{{ isFavorite ? '❤️' : '🤍' }}</text>
-            </view>
-            <button class="btn-cart" @click="onAddCart">加入购物车</button>
-          </view>
           <button class="btn-buy" @click="onBuy">立即购买</button>
         </view>
       </view>
@@ -61,15 +55,13 @@
 </template>
 
 <script>
-import { productDetail, productOrderCreate, productOrderPay, danshopProductDetail, danshopCartAdd, danshopFavoriteAdd, danshopFavoriteRemove } from '@/utils/api.js'
+import { productDetail, productOrderCreate, productOrderPay } from '@/utils/api.js'
 
 export default {
   data() {
     return {
       product: {},
-      loading: false,
-      isFavorite: false,
-      danshopProductId: null
+      loading: false
     }
   },
   computed: {
@@ -97,14 +89,6 @@ export default {
   onLoad(options) {
     if (options.id) {
       this.loadDetail(options.id)
-      if (options.danshopId) {
-        this.danshopProductId = parseInt(options.danshopId)
-        this.loadDanshopDetail(options.danshopId)
-      } else {
-        // For backward compat, treat id as danshopId
-        this.danshopProductId = parseInt(options.id)
-        this.loadDanshopDetail(options.id)
-      }
     }
   },
   methods: {
@@ -167,39 +151,6 @@ export default {
         uni.hideLoading()
         uni.showToast({ title: '支付失败', icon: 'none' })
       })
-    },
-    loadDanshopDetail(id) {
-      danshopProductDetail({ id: parseInt(id) }).then(res => {
-        if (res.code === 200 && res.data) {
-          this.product = res.data
-          this.isFavorite = res.data.isFavorite || false
-        }
-      }).catch(function() {})
-    },
-    onAddCart() {
-      if (!this.danshopProductId) {
-        uni.showToast({ title: '商品不可购买', icon: 'none' })
-        return
-      }
-      uni.showLoading({ title: '添加中...' })
-      danshopCartAdd({ productId: this.danshopProductId, quantity: 1 }).then(res => {
-        uni.hideLoading()
-        if (res.code === 200) {
-          uni.showToast({ title: '已加入购物车', icon: 'success' })
-        } else {
-          uni.showToast({ title: res.message || '添加失败', icon: 'none' })
-        }
-      }).catch(function() { uni.hideLoading() })
-    },
-    onToggleFavorite() {
-      if (!this.danshopProductId) return
-      var action = this.isFavorite ? danshopFavoriteRemove : danshopFavoriteAdd
-      action({ productId: this.danshopProductId }).then(res => {
-        if (res.code === 200) {
-          this.isFavorite = !this.isFavorite
-          uni.showToast({ title: this.isFavorite ? '已收藏' : '已取消收藏', icon: 'success' })
-        }
-      }).catch(function() {})
     }
   }
 }

+ 23 - 4
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -173,11 +173,26 @@ export default {
       })
     }
   },
-  onLoad() {
+  onLoad(options) {
+    var that = this
     var stored = uni.getStorageSync('checkoutItems')
-    if (stored) {
+    if (stored && stored.length > 0) {
       this.items = stored
       this.calcAmount()
+    } else if (options.productId) {
+      // Load product directly from detail page
+      var productData = {
+        productId: parseInt(options.productId),
+        productName: options.productName || '商品',
+        coverImage: options.coverImage || '',
+        unitPrice: parseInt(options.price) || 0,
+        quantity: parseInt(options.quantity) || 1
+      }
+      if (options.skuId) {
+        productData.skuId = parseInt(options.skuId)
+      }
+      this.items = [productData]
+      this.calcAmount()
     }
     this.loadDefaultAddress()
     this.loadCoupons()
@@ -282,10 +297,14 @@ export default {
       this.submitting = true
       var orderItems = []
       for (var i = 0; i < this.items.length; i++) {
-        orderItems.push({
+        var itemData = {
           productId: this.items[i].productId,
           quantity: this.items[i].quantity
-        })
+        }
+        if (this.items[i].skuId) {
+          itemData.skuId = this.items[i].skuId
+        }
+        orderItems.push(itemData)
       }
       var addressSnapshot = JSON.stringify({
         receiverName: this.selectedAddress.receiverName,

+ 90 - 57
cfc-frontend/pages/shop/detail/detail.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <view class="container">
     <view v-if="loading" class="loading-wrap">
       <text class="loading-text">加载中...</text>
@@ -14,13 +14,13 @@
           <text class="product-name">{{ product.name }}</text>
         </view>
         <view class="price-row">
-          <text class="price">{{ formatPriceWithSymbol(product.price) }}</text>
+          <text class="price">{{ formatPriceWithSymbol(currentPrice) }}</text>
           <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="meta-row">
           <text v-if="product.productType" class="meta-item">{{ typeLabel }}</text>
           <text v-if="product.domain" class="meta-item">{{ domainLabel }}</text>
-          <text class="meta-item">库存 {{ product.stock || 0 }}</text>
+          <text class="meta-item">库存 {{ currentStock }}</text>
         </view>
         <view v-if="product.brandName" class="vendor-row">
           <text class="vendor-label">品牌:</text>
@@ -28,6 +28,22 @@
         </view>
       </view>
 
+      <!-- SKU Selector -->
+      <view v-if="product.hasSku && skuList.length > 0" class="sku-section">
+        <text class="section-title">选择规格</text>
+        <view class="sku-list">
+          <view v-for="sku in skuList" :key="sku.id" class="sku-item">
+            <view
+              :class="['sku-chip', selectedSkuId === sku.id ? 'sku-chip-active' : '']"
+              @click="selectSku(sku)"
+            >
+              <text class="sku-chip-text">{{ getSkuDisplayName(sku) }}</text>
+              <text class="sku-chip-price">{{ formatPriceWithSymbol(sku.price) }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
       <view class="desc-section">
         <text class="section-title">商品介绍</text>
         <text class="desc-text">{{ product.description || '暂无介绍' }}</text>
@@ -40,14 +56,10 @@
 
       <view class="bottom-bar">
         <view class="price-info">
-          <text class="bottom-price">{{ formatPriceWithSymbol(product.price) }}</text>
+          <text class="bottom-price">{{ formatPriceWithSymbol(currentPrice) }}</text>
           <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="action-btns">
-          <view class="btn-favorite" @click="onToggleFavorite">
-            <text>{{ isFavorite ? '❤️' : '🤍' }}</text>
-          </view>
-          <button class="btn-cart" @click="onAddCart">加入购物车</button>
           <button class="btn-buy" @click="onBuy">立即购买</button>
         </view>
       </view>
@@ -59,15 +71,17 @@
 </template>
 
 <script>
-import { danshopProductDetail, danshopCartAdd, danshopFavoriteAdd, danshopFavoriteRemove } from '@/utils/api.js'
+import { productDetail, productSkuList } from '@/utils/api.js'
 
 export default {
   data() {
     return {
       product: {},
       loading: false,
-      isFavorite: false,
-      productId: null
+      productId: null,
+      skuList: [],
+      selectedSkuId: null,
+      selectedSku: null
     }
   },
   computed: {
@@ -78,6 +92,26 @@ export default {
     domainLabel() {
       var map = { action: '行动', mind: '心智', body: '身体', wisdom: '智慧', wealth: '财富' }
       return map[this.product.domain] || this.product.domain
+    },
+    currentPrice() {
+      if (this.selectedSku && this.selectedSku.price) {
+        return this.selectedSku.price
+      }
+      return this.product.price
+    },
+    currentStock() {
+      if (this.selectedSku && this.selectedSku.stock !== undefined) {
+        return this.selectedSku.stock
+      }
+      return this.product.stock || 0
+    },
+    skuSpecNames() {
+      if (!this.product.skuSpecNames) return []
+      try {
+        return JSON.parse(this.product.skuSpecNames)
+      } catch (e) {
+        return []
+      }
     }
   },
   onLoad(options) {
@@ -89,24 +123,43 @@ export default {
   methods: {
     loadDetail(id) {
       this.loading = true
-      danshopProductDetail({ id: parseInt(id) }).then(function(res) {
-        this.loading = false
+      var that = this
+      productDetail({ id: parseInt(id) }).then(function(res) {
+        that.loading = false
         if (res.code === 200 && res.data) {
-          this.product = res.data
-          this.isFavorite = res.data.isFavorite || false
+          that.product = res.data
+          if (that.product.hasSku) {
+            that.loadSkuList(id)
+          }
         }
-      }.bind(this)).catch(function() {
-        this.loading = false
-      }.bind(this))
+      }).catch(function() {
+        that.loading = false
+      })
     },
-    onAddCart() {
-      danshopCartAdd({ productId: parseInt(this.productId), quantity: 1 }).then(function(res) {
-        if (res.code === 200) {
-          uni.showToast({ title: '已加入购物车', icon: 'success' })
-        } else {
-          uni.showToast({ title: res.message || '添加失败', icon: 'none' })
+    loadSkuList(productId) {
+      var that = this
+      productSkuList({ productId: parseInt(productId) }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          that.skuList = res.data
         }
-      }.bind(this)).catch(function() {})
+      }).catch(function() {})
+    },
+    selectSku(sku) {
+      this.selectedSkuId = sku.id
+      this.selectedSku = sku
+    },
+    getSkuDisplayName(sku) {
+      if (!sku.specs) return ''
+      try {
+        var specs = JSON.parse(sku.specs)
+        var displayParts = []
+        for (var i = 0; i < specs.length; i++) {
+          displayParts.push(specs[i].value)
+        }
+        return displayParts.join(' / ')
+      } catch (e) {
+        return sku.skuCode || ''
+      }
     },
     onBuy() {
       var userId = uni.getStorageSync('userId')
@@ -114,19 +167,18 @@ export default {
         uni.navigateTo({ url: '/pages/login/login' })
         return
       }
-      uni.navigateTo({
-        url: '/pages/shop/checkout/checkout?productId=' + this.productId + '&quantity=1'
-      })
-    },
-    onToggleFavorite() {
-      var action = this.isFavorite ? danshopFavoriteRemove : danshopFavoriteAdd
-      var self = this
-      action({ productId: parseInt(this.productId) }).then(function(res) {
-        if (res.code === 200) {
-          self.isFavorite = !self.isFavorite
-          uni.showToast({ title: self.isFavorite ? '已收藏' : '已取消收藏', icon: 'success' })
-        }
-      }.bind(this)).catch(function() {})
+      if (this.product.hasSku && !this.selectedSkuId) {
+        uni.showToast({ title: '请选择规格', icon: 'none' })
+        return
+      }
+      var url = '/pages/shop/checkout/checkout?productId=' + this.productId + '&quantity=1'
+      url += '&productName=' + encodeURIComponent(this.product.name)
+      url += '&coverImage=' + encodeURIComponent(this.product.coverImage || '')
+      url += '&price=' + (this.selectedSku ? this.selectedSku.price : this.product.price)
+      if (this.selectedSkuId) {
+        url += '&skuId=' + this.selectedSkuId
+      }
+      uni.navigateTo({ url: url })
     }
   }
 }
@@ -264,25 +316,6 @@ export default {
   align-items: center;
   margin-left: auto;
 }
-.btn-favorite {
-  font-size: 40rpx;
-  margin-right: 16rpx;
-  padding: 0 8rpx;
-}
-.btn-cart {
-  background: #fff;
-  color: #F97316;
-  border: 2rpx solid #F97316;
-  font-size: 24rpx;
-  padding: 0 20rpx;
-  height: 60rpx;
-  line-height: 60rpx;
-  border-radius: 30rpx;
-  margin-right: 16rpx;
-}
-.btn-cart::after {
-  border: none;
-}
 .btn-buy {
   background: linear-gradient(135deg, #F97316, #FB923C);
   color: #fff;

+ 10 - 7
cfc-frontend/pages/shop/order-detail/order-detail.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <view class="container">
     <view v-if="loading" class="loading-wrap">
       <text class="loading-text">加载中...</text>
@@ -90,7 +90,7 @@
 </template>
 
 <script>
-import { danshopOrderDetail, danshopOrderConfirm } from '@/utils/api.js'
+import { productOrderDetail, productOrderConfirm } from '@/utils/api.js'
 
 export default {
   data() {
@@ -109,7 +109,7 @@ export default {
   methods: {
     loadDetail() {
       this.loading = true
-      danshopOrderDetail({ orderNo: this.orderNo }).then(res => {
+      productOrderDetail({ orderNo: this.orderNo }).then(res => {
         this.loading = false
         if (res.code === 200 && res.data) {
           this.order = res.data
@@ -119,7 +119,6 @@ export default {
       })
     },
     onPay() {
-      // Navigate to payment page — DanShop WeChat payment flow
       uni.navigateTo({
         url: '/pages/shop/payment/payment?orderNo=' + this.orderNo + '&amount=' + (this.order.totalAmount / 100 || 0)
       })
@@ -130,7 +129,7 @@ export default {
         content: '确定取消该订单吗?',
         success: (confirm) => {
           if (confirm.confirm) {
-            danshopOrderCancel({ orderNo: this.orderNo }).then(res => {
+            productOrderCancel({ orderNo: this.orderNo }).then(res => {
               if (res.code === 200) {
                 uni.showToast({ title: '订单已取消', icon: 'success' })
                 this.loadDetail()
@@ -153,7 +152,7 @@ export default {
         content: '确定已收到商品吗?',
         success: (confirm) => {
           if (confirm.confirm) {
-            danshopOrderConfirm({ orderNo: this.orderNo }).then(res => {
+            productOrderConfirm({ orderNo: this.orderNo }).then(res => {
               if (res.code === 200) {
                 uni.showToast({ title: '已确认收货', icon: 'success' })
                 this.loadDetail()
@@ -182,7 +181,7 @@ export default {
       return map[status] || status
     },
     paymentLabel(method) {
-      const map = { wechat: '微信支付', points: '积分支付', membership: '会员抵扣' }
+      const map = { wechat: '微信支付', points: '积分支付', membership: '会员折扣' }
       return map[method] || method
     },
     typeLabel(type) {
@@ -199,6 +198,10 @@ export default {
       if (!time) return ''
       const d = new Date(time)
       return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
+    },
+    formatPriceWithSymbol(price) {
+      if (!price && price !== 0) return '-'
+      return '¥' + (price / 100).toFixed(2)
     }
   }
 }

+ 45 - 14
cfc-frontend/pages/shop/order-list/order-list.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <view class="container">
     <view class="status-tabs">
       <view
@@ -23,7 +23,7 @@
         <text class="loading-text">加载中...</text>
       </view>
       <view v-else-if="orders.length === 0" class="empty-wrap">
-        <text class="empty-icon">📋</text>
+        <text class="empty-icon">📦</text>
         <text class="empty-text">暂无订单</text>
       </view>
       <view v-else>
@@ -61,10 +61,15 @@
                 @click.stop="onPay(item.orderNo, item.totalAmount / 100)"
               >支付</button>
               <button
-                v-if="item.status === 'paid'"
+                v-if="item.status === 'shipped'"
                 class="btn-small btn-confirm"
                 @click.stop="onConfirm(item.orderNo)"
-              >确认完成</button>
+              >确认收货</button>
+              <button
+                v-if="(item.status === 'paid' || item.status === 'shipped') && item.refundStatus !== 1 && item.refundStatus !== 2"
+                class="btn-small btn-refund"
+                @click.stop="onApplyRefund(item.orderNo)"
+              >申请退款</button>
             </view>
           </view>
         </view>
@@ -73,14 +78,14 @@
         <text class="loading-text">加载中...</text>
       </view>
       <view v-if="noMore && orders.length > 0" class="no-more">
-        <text class="no-more-text">— 没有更多了 —</text>
+        <text class="no-more-text">~没有更多了~</text>
       </view>
     </scroll-view>
   </view>
 </template>
 
 <script>
-import { danshopOrderList, danshopOrderCancel, danshopOrderConfirm } from '@/utils/api.js'
+import { productOrderMy, productOrderCancel, productOrderConfirm, productOrderRefundApply } from '@/utils/api.js'
 
 export default {
   data() {
@@ -90,7 +95,8 @@ export default {
         { label: '待支付', value: 'pending' },
         { label: '待发货', value: 'paid' },
         { label: '待收货', value: 'shipped' },
-        { label: '已完成', value: 'completed' }
+        { label: '已完成', value: 'completed' },
+        { label: '退款中', value: 'refunding' }
       ],
       currentStatus: '',
       orders: [],
@@ -120,7 +126,7 @@ export default {
       if (this.currentStatus) {
         params.status = this.currentStatus
       }
-      danshopOrderList(params).then(res => {
+      productOrderMy(params).then(res => {
         this.loading = false
         this.refreshing = false
         if (res.code === 200 && res.data) {
@@ -151,7 +157,7 @@ export default {
       if (this.currentStatus) {
         params.status = this.currentStatus
       }
-      danshopOrderList(params).then(res => {
+      productOrderMy(params).then(res => {
         this.loadingMore = false
         if (res.code === 200 && res.data) {
           const list = res.data || []
@@ -166,7 +172,6 @@ export default {
       uni.navigateTo({ url: '/pages/shop/order-detail/order-detail?orderNo=' + orderNo })
     },
     onPay(orderNo, amount) {
-      // Navigate to payment page — DanShop payment flow (same as checkout)
       uni.navigateTo({
         url: '/pages/shop/payment/payment?orderNo=' + orderNo + '&amount=' + (amount || 0)
       })
@@ -177,7 +182,7 @@ export default {
         content: '确定取消该订单吗?',
         success: (confirm) => {
           if (confirm.confirm) {
-            danshopOrderCancel({ orderNo }).then(res => {
+            productOrderCancel({ orderNo }).then(res => {
               if (res.code === 200) {
                 uni.showToast({ title: '订单已取消', icon: 'success' })
                 this.onRefresh()
@@ -190,23 +195,45 @@ export default {
       })
     },
     onConfirm(orderNo) {
-      danshopOrderConfirm({ orderNo }).then(res => {
+      productOrderConfirm({ orderNo }).then(res => {
         if (res.code === 200) {
-          uni.showToast({ title: '订单已完成', icon: 'success' })
+          uni.showToast({ title: '收货成功', icon: 'success' })
           this.onRefresh()
         } else {
           uni.showToast({ title: res.message || '操作失败', icon: 'none' })
         }
       })
     },
+    onApplyRefund(orderNo) {
+      uni.showModal({
+        title: '申请退款',
+        content: '确定要申请退款吗?',
+        success: (confirm) => {
+          if (confirm.confirm) {
+            productOrderRefundApply({ orderNo: orderNo }).then(res => {
+              if (res.code === 200) {
+                uni.showToast({ title: '退款申请已提交', icon: 'success' })
+                this.onRefresh()
+              } else {
+                uni.showToast({ title: res.message || '申请失败', icon: 'none' })
+              }
+            })
+          }
+        }
+      })
+    },
     statusLabel(status) {
       const map = {
         pending: '待支付',
         paid: '待发货',
+        pending_shipment: '待发货',
         shipped: '待收货',
+        pending_receipt: '待收货',
         completed: '已完成',
         refunded: '已退款',
-        cancelled: '已取消'
+        refunding: '退款中',
+        cancelled: '已取消',
+        abnormal: '异常'
       }
       return map[status] || status
     },
@@ -220,6 +247,10 @@ export default {
       }
       return map[type] || type
     },
+    formatPriceWithSymbol(amount) {
+      if (!amount && amount !== 0) return ''
+      return '¥' + (amount / 100).toFixed(2)
+    },
     formatTime(time) {
       if (!time) return ''
       const d = new Date(time)

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 171 - 173
cfc-frontend/utils/api.js


+ 0 - 46
cfc-web/src/api/danshop.js

@@ -1,46 +0,0 @@
-import request from '@/utils/request'
-
-const DANSHOP_BASE = 'http://localhost:9081'
-
-const danshopRequest = (url, method = 'post', data = {}) => {
-  return request({
-    url: DANSHOP_BASE + url,
-    method,
-    data
-  })
-}
-
-// ========== 分类管理 ==========
-export const danshopCategoryList = (data) => danshopRequest('/api/admin/shop/category/list', 'post', data)
-export const danshopCategorySave = (data) => {
-  if (data.id) {
-    return danshopRequest('/api/admin/shop/category/update', 'post', data)
-  }
-  return danshopRequest('/api/admin/shop/category/create', 'post', data)
-}
-export const danshopCategoryDelete = (data) => danshopRequest('/api/admin/shop/category/delete', 'post', data)
-
-// ========== 分润规则 ==========
-export const danshopProfitRuleList = (data) => danshopRequest('/api/admin/shop/profitRule/list', 'post', data)
-export const danshopProfitRuleSave = (data) => {
-  if (data.id) {
-    return danshopRequest('/api/admin/shop/profitRule/update', 'post', data)
-  }
-  return danshopRequest('/api/admin/shop/profitRule/create', 'post', data)
-}
-export const danshopProfitRuleDelete = (data) => danshopRequest('/api/admin/shop/profitRule/delete', 'post', data)
-
-// ========== 商品管理 (DanShop) ==========
-export const danshopProductList = (data) => danshopRequest('/api/admin/shop/product/list', 'post', data)
-export const danshopProductReview = (data) => danshopRequest('/api/admin/shop/product/review', 'post', data)
-export const danshopProductSetProfitRule = (data) => danshopRequest('/api/admin/shop/product/setProfitRule', 'post', data)
-
-// ========== 订单管理 (DanShop) ==========
-export const danshopOrderList = (data) => danshopRequest('/api/admin/shop/order/list', 'post', data)
-export const danshopOrderProfitLogs = (data) => danshopRequest('/api/admin/shop/order/profitLogs', 'post', data)
-
-// ========== 物流管理 ==========
-export const danshopLogisticsList = (data) => danshopRequest('/api/admin/shop/logistics/list', 'post', data)
-export const danshopLogisticsUpdate = (data) => danshopRequest('/api/admin/shop/logistics/update', 'post', data)
-export const danshopLogisticsShip = (data) => danshopRequest('/api/seller/order/ship', 'post', data)
-export const danshopLogisticsQuery = (data) => danshopRequest('/api/shop/logistics/query', 'post', data)

+ 49 - 0
cfc-web/src/api/energy.js

@@ -8,3 +8,52 @@ export function getFamilyEnergySandbox() {
     data: {}
   })
 }
+
+// 能量规则管理
+export function getEnergyRuleList(params) {
+  return request({
+    url: '/api/admin/energy-rule/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function createEnergyRule(data) {
+  return request({
+    url: '/api/admin/energy-rule/create',
+    method: 'post',
+    data: data
+  })
+}
+
+export function updateEnergyRule(data) {
+  return request({
+    url: '/api/admin/energy-rule/update',
+    method: 'post',
+    data: data
+  })
+}
+
+export function deleteEnergyRule(id) {
+  return request({
+    url: '/api/admin/energy-rule/delete',
+    method: 'post',
+    data: { id }
+  })
+}
+
+export function toggleEnergyRule(id) {
+  return request({
+    url: '/api/admin/energy-rule/toggle',
+    method: 'post',
+    data: { id }
+  })
+}
+
+export function getEnergyDimensions() {
+  return request({
+    url: '/api/admin/energy-rule/dimensions',
+    method: 'post',
+    data: {}
+  })
+}

+ 13 - 31
cfc-web/src/router/index.js

@@ -221,6 +221,12 @@ const routes = [
       name: 'EnergySandbox',
       component: () => import('@/views/admin/EnergySandbox.vue'),
       meta: { title: '五维能量', perm: 'energy' }
+    },
+    {
+      path: 'energy-rule',
+      name: 'EnergyRuleManagement',
+      component: () => import('@/views/admin/EnergyRuleManagement.vue'),
+      meta: { title: '能量规则管理', perm: 'energy' }
     },
       // ========== 文章管理 ==========
       {
@@ -284,37 +290,7 @@ const routes = [
         component: () => import('@/views/admin/RelationshipTypes.vue'),
         meta: { title: '关系类型配置', perm: 'system:config' }
       },
-      // ========== DanShop 电商管理 ==========
-      {
-        path: 'danshop-category',
-        name: 'DanShopCategory',
-        component: () => import('@/views/admin/DanShopCategoryManage'),
-        meta: { title: '商品分类管理', perm: 'danshop:category' }
-      },
-      {
-        path: 'danshop-profit-rule',
-        name: 'DanShopProfitRule',
-        component: () => import('@/views/admin/DanShopProfitRuleConfig'),
-        meta: { title: '分润规则配置', perm: 'danshop:profit-rule' }
-      },
-      {
-        path: 'danshop-logistics',
-        name: 'DanShopLogistics',
-        component: () => import('@/views/admin/DanShopLogisticsManage'),
-        meta: { title: '物流管理', perm: 'danshop:logistics' }
-      },
-      {
-        path: 'danshop-product',
-        name: 'DanShopProduct',
-        component: () => import('@/views/admin/ProductManage.vue'),
-        meta: { title: 'DanShop商品', perm: 'danshop:product' }
-      },
-      {
-        path: 'danshop-order',
-        name: 'DanShopOrder',
-        component: () => import('@/views/admin/OrderManage.vue'),
-        meta: { title: 'DanShop订单', perm: 'danshop:order' }
-      },
+
       // ========== 饮食管理 (admin) ==========
       {
         path: 'foods',
@@ -388,6 +364,12 @@ const routes = [
         component: () => import('@/views/admin/ActivityReview.vue'),
         meta: { title: '活动审核', perm: 'activity:review' }
       },
+      {
+        path: 'activity-registration-review',
+        name: 'ActivityRegistrationReview',
+        component: () => import('@/views/admin/ActivityRegistrationReview.vue'),
+        meta: { title: '活动报名审核', perm: 'activity:review' }
+      },
       // ========== 维度配置 + 知识库管理 ==========
       {
         path: 'coupon',

+ 3 - 3
cfc-web/src/views/Layout.vue

@@ -174,9 +174,7 @@ export default {
             { path: '/product-profit-rate', label: '产品利润率', icon: 'el-icon-data-line', perm: 'commerce:profit' },
             { path: '/coupon', label: '优惠券管理', icon: 'el-icon-ticket', perm: 'marketing:coupon' },
             { path: '/promotion', label: '推广管理', icon: 'el-icon-s-marketing', perm: 'marketing:promotion' },
-            { path: '/danshop-category', label: 'DanShop分类', icon: 'el-icon-menu', perm: 'danshop:category' },
-            { path: '/danshop-profit-rule', label: 'DanShop分润', icon: 'el-icon-s-finance', perm: 'danshop:profit' },
-            { path: '/danshop-logistics', label: 'DanShop物流', icon: 'el-icon-truck', perm: 'danshop:logistics' },
+
           ]},
 
         // ===== 6. 内容中心 (admin + article_manager + activity_manager) =====
@@ -186,6 +184,7 @@ export default {
             { path: '/article-manage', label: '文章管理', icon: 'el-icon-document', perm: 'articles:manage' },
             { path: '/activities', label: '活动列表', icon: 'el-icon-date', perm: 'activity:list' },
             { path: '/activity-review', label: '活动审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
+            { path: '/activity-registration-review', label: '活动报名审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
           ]},
 
         // ===== 7. 健康饮食 (admin + nutritionist) =====
@@ -196,6 +195,7 @@ export default {
             { path: '/health-checkins', label: '健康打卡', icon: 'el-icon-s-order', perm: 'health:checkins' },
             { path: '/nutrition-mappings', label: '营养素映射', icon: 'el-icon-connection', perm: 'health:mappings' },
             { path: '/energy-sandbox', label: '五维能量', icon: 'el-icon-data-line', perm: 'energy' },
+            { path: '/energy-rule', label: '能量规则管理', icon: 'el-icon-setting', perm: 'energy' },
             { path: '/foods', label: '食材管理', icon: 'el-icon-apple', perm: 'diet:foods' },
             { path: '/recipes', label: '食谱管理', icon: 'el-icon-dish', perm: 'diet:recipes' },
             { path: '/seasonal-foods', label: '应季食材', icon: 'el-icon-sunny', perm: 'diet:seasonal' },

+ 0 - 251
cfc-web/src/views/admin/DanShopCategoryManage.vue

@@ -1,251 +0,0 @@
-<template>
-  <div class="danshop-category">
-    <div class="header">
-      <h2>商品分类管理</h2>
-      <div class="header-actions">
-        <el-input
-          v-model="searchName"
-          placeholder="分类名称"
-          style="width: 200px; margin-right: 10px;"
-          clearable
-          @keyup.enter.native="handleSearch"
-        />
-        <el-button type="primary" @click="handleSearch">搜索</el-button>
-        <el-button type="success" @click="handleCreate" style="margin-left: 10px;">+ 新增分类</el-button>
-      </div>
-    </div>
-
-    <el-table :data="list" v-loading="loading" border stripe>
-      <el-table-column prop="id" label="ID" width="70" />
-      <el-table-column prop="name" label="分类名称" min-width="150" show-overflow-tooltip />
-      <el-table-column label="上级分类" width="150" show-overflow-tooltip>
-        <template slot-scope="{ row }">
-          {{ row.parentName || '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column prop="sortOrder" label="排序" width="80" />
-      <el-table-column label="状态" width="90">
-        <template slot-scope="{ row }">
-          <el-switch
-            v-model="row.isActive"
-            :disabled="statusLoading === row.id"
-            @change="(val) => handleStatusChange(row, val)"
-          />
-        </template>
-      </el-table-column>
-      <el-table-column label="创建时间" width="160">
-        <template slot-scope="{ row }">
-          {{ formatTime(row.createTime) }}
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" width="160" fixed="right">
-        <template slot-scope="{ row }">
-          <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-          <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <el-pagination
-      @size-change="onPageChange"
-      @current-change="onPageChange"
-      :current-page="page"
-      :page-size="size"
-      :total="total"
-      layout="total, prev, pager, next"
-      style="margin-top: 20px; text-align: right;"
-    />
-
-    <!-- 新增/编辑分类弹窗 -->
-    <el-dialog :title="isEdit ? '编辑分类' : '新增分类'" :visible.sync="dialogVisible" width="500px">
-      <el-form :model="form" label-width="100px">
-        <el-form-item label="分类名称">
-          <el-input v-model="form.name" placeholder="请输入分类名称" />
-        </el-form-item>
-        <el-form-item label="上级分类">
-          <el-select v-model="form.parentId" placeholder="请选择上级分类" clearable style="width: 100%;">
-            <el-option label="无(顶级分类)" :value="0" />
-            <el-option
-              v-for="cat in parentOptions"
-              :key="cat.id"
-              :label="cat.name"
-              :value="cat.id"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="排序">
-          <el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
-        </el-form-item>
-        <el-form-item label="分类图标">
-          <el-input v-model="form.image" placeholder="请输入图标URL" />
-        </el-form-item>
-        <el-form-item label="启用状态">
-          <el-switch v-model="form.isActive" />
-        </el-form-item>
-      </el-form>
-      <span slot="footer">
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmit" :loading="submitLoading">保存</el-button>
-      </span>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { danshopCategoryList, danshopCategorySave, danshopCategoryDelete } from '@/api/danshop'
-
-export default {
-  name: 'DanShopCategory',
-  data() {
-    return {
-      list: [],
-      loading: false,
-      page: 1,
-      size: 20,
-      total: 0,
-      searchName: '',
-      statusLoading: null,
-      dialogVisible: false,
-      isEdit: false,
-      submitLoading: false,
-      form: {
-        id: null,
-        name: '',
-        parentId: 0,
-        sortOrder: 0,
-        image: '',
-        isActive: true
-      },
-      parentOptions: []
-    }
-  },
-  mounted() {
-    this.loadList()
-  },
-  methods: {
-    async loadList() {
-      this.loading = true
-      try {
-        const res = await danshopCategoryList({
-          page: this.page,
-          size: this.size,
-          name: this.searchName || undefined
-        })
-        this.list = (res.data && res.data.list) || []
-        this.total = (res.data && res.data.total) || 0
-        this.parentOptions = this.list.filter(c => c.parentId === 0 || !c.parentId)
-      } catch (e) {
-        this.$message.error('加载分类列表失败')
-      } finally {
-        this.loading = false
-      }
-    },
-    onPageChange(val) {
-      this.page = val
-      this.loadList()
-    },
-    handleSearch() {
-      this.page = 1
-      this.loadList()
-    },
-    handleCreate() {
-      this.isEdit = false
-      this.form = { id: null, name: '', parentId: 0, sortOrder: 0, image: '', isActive: true }
-      this.dialogVisible = true
-    },
-    handleEdit(row) {
-      this.isEdit = true
-      this.form = {
-        id: row.id,
-        name: row.name,
-        parentId: row.parentId || 0,
-        sortOrder: row.sortOrder || 0,
-        image: row.image || '',
-        isActive: row.isActive !== undefined ? row.isActive : true
-      }
-      this.dialogVisible = true
-    },
-    async handleSubmit() {
-      if (!this.form.name.trim()) {
-        return this.$message.warning('请输入分类名称')
-      }
-      this.submitLoading = true
-      try {
-        await danshopCategorySave({
-          id: this.form.id || undefined,
-          name: this.form.name,
-          parentId: this.form.parentId || 0,
-          sortOrder: this.form.sortOrder,
-          image: this.form.image,
-          isActive: this.form.isActive
-        })
-        this.$message.success(this.isEdit ? '分类已更新' : '分类已创建')
-        this.dialogVisible = false
-        this.loadList()
-      } catch (e) {
-        this.$message.error('保存失败')
-      } finally {
-        this.submitLoading = false
-      }
-    },
-    async handleDelete(row) {
-      try {
-        await this.$confirm(`确认删除分类「${row.name}」?删除后不可恢复。`, '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning'
-        })
-        await danshopCategoryDelete({ id: row.id })
-        this.$message.success('删除成功')
-        this.loadList()
-      } catch (e) {
-        if (e !== 'cancel') {
-          this.$message.error('删除失败')
-        }
-      }
-    },
-    async handleStatusChange(row, val) {
-      this.statusLoading = row.id
-      try {
-        await danshopCategorySave({
-          id: row.id,
-          name: row.name,
-          parentId: row.parentId || 0,
-          sortOrder: row.sortOrder || 0,
-          image: row.image || '',
-          isActive: val
-        })
-        this.$message.success(val ? '已启用' : '已停用')
-      } catch (e) {
-        row.isActive = !val
-        this.$message.error('状态更新失败')
-      } finally {
-        this.statusLoading = null
-      }
-    },
-    formatTime(time) {
-      if (!time) return '-'
-      return new Date(time).toLocaleString()
-    }
-  }
-}
-</script>
-
-<style scoped>
-.danshop-category {
-  padding: 20px;
-}
-.header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20px;
-}
-.header h2 {
-  margin: 0;
-}
-.header-actions {
-  display: flex;
-  align-items: center;
-}
-</style>

+ 0 - 301
cfc-web/src/views/admin/DanShopLogisticsManage.vue

@@ -1,301 +0,0 @@
-<template>
-  <div class="danshop-logistics">
-    <div class="header">
-      <h2>物流管理</h2>
-      <div class="header-actions">
-        <el-input
-          v-model="searchOrderNo"
-          placeholder="订单号"
-          style="width: 180px; margin-right: 10px;"
-          clearable
-          @keyup.enter.native="handleSearch"
-        />
-        <el-select v-model="searchStatus" placeholder="物流状态" @change="handleSearch" style="width: 140px; margin-right: 10px;">
-          <el-option label="全部" value="" />
-          <el-option label="待发货" value="pending" />
-          <el-option label="已发货" value="shipped" />
-          <el-option label="运输中" value="signing" />
-          <el-option label="已签收" value="completed" />
-        </el-select>
-        <el-button type="primary" @click="handleSearch">搜索</el-button>
-      </div>
-    </div>
-
-    <el-table :data="list" v-loading="loading" border stripe>
-      <el-table-column label="订单号" width="180" show-overflow-tooltip>
-        <template slot-scope="{ row }">
-          {{ row.orderNo || row.orderId || '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column prop="logisticsCompany" label="物流公司" width="120" show-overflow-tooltip />
-      <el-table-column prop="logisticsNo" label="运单号" width="150" show-overflow-tooltip />
-      <el-table-column prop="receiverName" label="收件人" width="100" />
-      <el-table-column prop="receiverPhone" label="收件人电话" width="130" />
-      <el-table-column prop="receiverAddress" label="收件人地址" min-width="180" show-overflow-tooltip />
-      <el-table-column label="物流状态" width="100">
-        <template slot-scope="{ row }">
-          <el-tag :type="statusTag(row.logisticsStatus)">{{ statusLabel(row.logisticsStatus) }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="发货时间" width="160">
-        <template slot-scope="{ row }">
-          {{ formatTime(row.shipTime) }}
-        </template>
-      </el-table-column>
-      <el-table-column label="签收时间" width="160">
-        <template slot-scope="{ row }">
-          {{ formatTime(row.signTime) }}
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" width="160" fixed="right">
-        <template slot-scope="{ row }">
-          <el-button type="primary" size="small" @click="viewDetail(row)">查看详情</el-button>
-          <el-button
-            v-if="row.logisticsStatus === 'pending'"
-            type="success"
-            size="small"
-            @click="showShipDialog(row)"
-          >发货</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <el-pagination
-      @size-change="onPageChange"
-      @current-change="onPageChange"
-      :current-page="page"
-      :page-size="size"
-      :total="total"
-      layout="total, prev, pager, next"
-      style="margin-top: 20px; text-align: right;"
-    />
-
-    <!-- 物流详情弹窗 -->
-    <el-dialog title="物流详情" :visible.sync="detailDialogVisible" width="600px">
-      <div v-loading="detailLoading" v-if="detail">
-        <el-descriptions :column="2" border style="margin-bottom: 20px;">
-          <el-descriptions-item label="物流公司">{{ detail.logisticsCompany || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="运单号">{{ detail.logisticsNo || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="订单号">{{ detail.orderNo || detail.orderId || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="物流状态">
-            <el-tag :type="statusTag(detail.logisticsStatus)">{{ statusLabel(detail.logisticsStatus) }}</el-tag>
-          </el-descriptions-item>
-          <el-descriptions-item label="发件人">{{ detail.senderName || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="发件人电话">{{ detail.senderPhone || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="收件人">{{ detail.receiverName || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="收件人电话">{{ detail.receiverPhone || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="收件人地址" :span="2">{{ detail.receiverAddress || '-' }}</el-descriptions-item>
-        </el-descriptions>
-
-        <h4 style="margin: 0 0 12px 0;">物流跟踪</h4>
-        <div class="tracking-timeline">
-          <el-timeline v-if="trackingList.length > 0">
-            <el-timeline-item
-              v-for="(item, index) in trackingList"
-              :key="index"
-              :timestamp="formatTime(item.time)"
-              :color="index === 0 ? '#E6A23C' : '#C0C4CC'"
-            >
-              <p style="margin: 0;">{{ item.status || '-' }}</p>
-              <p style="margin: 4px 0 0; font-size: 12px; color: #909399;">{{ item.content || '-' }}</p>
-            </el-timeline-item>
-          </el-timeline>
-          <div v-else class="no-tracking">
-            <i class="el-icon-info"></i>
-            <p>暂无物流跟踪信息</p>
-          </div>
-        </div>
-      </div>
-      <span slot="footer">
-        <el-button @click="detailDialogVisible = false">关闭</el-button>
-      </span>
-    </el-dialog>
-
-    <!-- 发货弹窗 -->
-    <el-dialog title="发货" :visible.sync="shipDialogVisible" width="450px">
-      <el-form :model="shipForm" label-width="100px">
-        <el-form-item label="物流公司">
-          <el-select v-model="shipForm.logisticsCompany" placeholder="请选择物流公司" style="width: 100%;">
-            <el-option label="顺丰速运" value="顺丰速运" />
-            <el-option label="圆通速递" value="圆通速递" />
-            <el-option label="中通快递" value="中通快递" />
-            <el-option label="申通快递" value="申通快递" />
-            <el-option label="韵达快递" value="韵达快递" />
-            <el-option label="天天快递" value="天天快递" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="运单号">
-          <el-input v-model="shipForm.logisticsNo" placeholder="请输入运单号" />
-        </el-form-item>
-      </el-form>
-      <span slot="footer">
-        <el-button @click="shipDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmShip" :loading="shipLoading">确认发货</el-button>
-      </span>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { danshopLogisticsList, danshopLogisticsShip, danshopLogisticsQuery } from '@/api/danshop'
-
-export default {
-  name: 'DanShopLogistics',
-  data() {
-    return {
-      list: [],
-      loading: false,
-      page: 1,
-      size: 20,
-      total: 0,
-      searchOrderNo: '',
-      searchStatus: '',
-      detailDialogVisible: false,
-      detailLoading: false,
-      detail: null,
-      trackingList: [],
-      shipDialogVisible: false,
-      shipLoading: false,
-      shipTarget: null,
-      shipForm: {
-        logisticsCompany: '',
-        logisticsNo: ''
-      }
-    }
-  },
-  mounted() {
-    this.loadList()
-  },
-  methods: {
-    async loadList() {
-      this.loading = true
-      try {
-        const params = {
-          page: this.page,
-          size: this.size
-        }
-        if (this.searchOrderNo.trim()) {
-          params.orderNo = this.searchOrderNo.trim()
-        }
-        if (this.searchStatus) {
-          params.logisticsStatus = this.searchStatus
-        }
-        const res = await danshopLogisticsList(params)
-        this.list = (res.data && res.data.list) || []
-        this.total = (res.data && res.data.total) || 0
-      } catch (e) {
-        this.$message.error('加载物流列表失败')
-      } finally {
-        this.loading = false
-      }
-    },
-    onPageChange(val) {
-      this.page = val
-      this.loadList()
-    },
-    handleSearch() {
-      this.page = 1
-      this.loadList()
-    },
-    async viewDetail(row) {
-      this.detail = row
-      this.trackingList = []
-      this.detailDialogVisible = true
-      this.detailLoading = true
-      try {
-        const res = await danshopLogisticsQuery({ orderId: row.orderId || row.id })
-        if (res.data) {
-          this.trackingList = res.data.trackingList || res.data.tracks || []
-          // Merge any additional detail fields from the query response
-          if (res.data.logisticsCompany) this.detail.logisticsCompany = res.data.logisticsCompany
-          if (res.data.logisticsNo) this.detail.logisticsNo = res.data.logisticsNo
-          if (res.data.receiverName) this.detail.receiverName = res.data.receiverName
-          if (res.data.receiverPhone) this.detail.receiverPhone = res.data.receiverPhone
-          if (res.data.receiverAddress) this.detail.receiverAddress = res.data.receiverAddress
-          if (res.data.senderName) this.detail.senderName = res.data.senderName
-          if (res.data.senderPhone) this.detail.senderPhone = res.data.senderPhone
-        }
-      } catch (e) {
-        // Tracking info is optional, still show basic detail
-      } finally {
-        this.detailLoading = false
-      }
-    },
-    showShipDialog(row) {
-      this.shipTarget = row
-      this.shipForm = { logisticsCompany: '', logisticsNo: '' }
-      this.shipDialogVisible = true
-    },
-    async confirmShip() {
-      if (!this.shipForm.logisticsCompany) {
-        return this.$message.warning('请选择物流公司')
-      }
-      if (!this.shipForm.logisticsNo.trim()) {
-        return this.$message.warning('请输入运单号')
-      }
-      this.shipLoading = true
-      try {
-        await danshopLogisticsShip({
-          orderId: this.shipTarget.orderId || this.shipTarget.id,
-          logisticsCompany: this.shipForm.logisticsCompany,
-          logisticsNo: this.shipForm.logisticsNo.trim()
-        })
-        this.$message.success('发货成功')
-        this.shipDialogVisible = false
-        this.loadList()
-      } catch (e) {
-        this.$message.error('发货失败')
-      } finally {
-        this.shipLoading = false
-      }
-    },
-    statusTag(status) {
-      const map = { pending: 'warning', shipped: 'primary', signing: 'warning', completed: 'success' }
-      return map[status] || 'info'
-    },
-    statusLabel(status) {
-      const map = { pending: '待发货', shipped: '已发货', signing: '运输中', completed: '已签收' }
-      return map[status] || status || '-'
-    },
-    formatTime(time) {
-      if (!time) return '-'
-      return new Date(time).toLocaleString()
-    }
-  }
-}
-</script>
-
-<style scoped>
-.danshop-logistics {
-  padding: 20px;
-}
-.header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20px;
-}
-.header h2 {
-  margin: 0;
-}
-.header-actions {
-  display: flex;
-  align-items: center;
-}
-.tracking-timeline {
-  max-height: 400px;
-  overflow-y: auto;
-}
-.no-tracking {
-  text-align: center;
-  padding: 40px 0;
-  color: #909399;
-}
-.no-tracking i {
-  font-size: 36px;
-  margin-bottom: 12px;
-}
-.no-tracking p {
-  margin: 0;
-}
-</style>

+ 0 - 359
cfc-web/src/views/admin/DanShopProfitRuleConfig.vue

@@ -1,359 +0,0 @@
-<template>
-  <div class="profit-rule-config">
-    <div class="header">
-      <h2>分润规则配置</h2>
-      <div class="header-actions">
-        <el-input
-          v-model="searchName"
-          placeholder="规则名称"
-          style="width: 200px; margin-right: 10px;"
-          clearable
-          @keyup.enter.native="handleSearch"
-        />
-        <el-button type="primary" @click="handleSearch">搜索</el-button>
-        <el-button type="success" @click="handleCreate" style="margin-left: 10px;">+ 新增规则</el-button>
-      </div>
-    </div>
-
-    <el-table :data="list" v-loading="loading" border stripe>
-      <el-table-column prop="ruleName" label="规则名称" min-width="140" show-overflow-tooltip />
-      <el-table-column label="规则类型" width="130">
-        <template slot-scope="{ row }">
-          <el-tag :type="ruleTypeTag(row.ruleType)">{{ ruleTypeLabel(row.ruleType) }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="关联目标" width="150" show-overflow-tooltip>
-        <template slot-scope="{ row }">
-          {{ row.targetName || '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="平台利润率(%)" width="130">
-        <template slot-scope="{ row }">
-          {{ row.platformProfitRate != null ? row.platformProfitRate + '%' : '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="L1推荐人比例(%)" width="140">
-        <template slot-scope="{ row }">
-          {{ row.referrerL1Rate != null ? row.referrerL1Rate + '%' : '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="L2推荐人比例(%)" width="140">
-        <template slot-scope="{ row }">
-          {{ row.referrerL2Rate != null ? row.referrerL2Rate + '%' : '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="服务费率(%)" width="120">
-        <template slot-scope="{ row }">
-          {{ row.platformServiceRate != null ? row.platformServiceRate + '%' : '-' }}
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" width="80">
-        <template slot-scope="{ row }">
-          <el-switch
-            v-model="row.isActive"
-            :disabled="statusLoading === row.id"
-            @change="(val) => handleStatusChange(row, val)"
-          />
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" width="150" fixed="right">
-        <template slot-scope="{ row }">
-          <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
-          <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <el-pagination
-      @size-change="onPageChange"
-      @current-change="onPageChange"
-      :current-page="page"
-      :page-size="size"
-      :total="total"
-      layout="total, prev, pager, next"
-      style="margin-top: 20px; text-align: right;"
-    />
-
-    <!-- 新增/编辑规则弹窗 -->
-    <el-dialog :title="isEdit ? '编辑规则' : '新增规则'" :visible.sync="dialogVisible" width="600px">
-      <el-form :model="form" label-width="140px">
-        <el-form-item label="规则名称">
-          <el-input v-model="form.ruleName" placeholder="请输入规则名称" />
-        </el-form-item>
-        <el-form-item label="规则类型">
-          <el-select v-model="form.ruleType" placeholder="请选择规则类型" style="width: 100%;" @change="onRuleTypeChange">
-            <el-option label="商品分类" value="product_category" />
-            <el-option label="供应商等级" value="vendor_level" />
-            <el-option label="全局" value="global" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="关联目标" v-if="form.ruleType === 'product_category'">
-          <el-select v-model="form.targetId" placeholder="请选择商品分类" clearable style="width: 100%;">
-            <el-option
-              v-for="cat in categoryOptions"
-              :key="cat.id"
-              :label="cat.name"
-              :value="cat.id"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="关联目标" v-if="form.ruleType === 'vendor_level'">
-          <el-input v-model="form.targetName" placeholder="请输入供应商等级名称" />
-          <input type="hidden" v-model="form.targetId" />
-        </el-form-item>
-        <el-form-item label="平台利润率(%)">
-          <el-input-number
-            v-model="form.platformProfitRate"
-            :min="0"
-            :max="100"
-            :precision="2"
-            style="width: 200px;"
-          />
-          <span style="margin-left: 8px;">%</span>
-        </el-form-item>
-        <el-form-item label="L1推荐人比例(%)">
-          <el-input-number
-            v-model="form.referrerL1Rate"
-            :min="0"
-            :max="100"
-            :precision="2"
-            style="width: 200px;"
-          />
-          <span style="margin-left: 8px;">%</span>
-        </el-form-item>
-        <el-form-item label="L2推荐人比例(%)">
-          <el-input-number
-            v-model="form.referrerL2Rate"
-            :min="0"
-            :max="100"
-            :precision="2"
-            style="width: 200px;"
-          />
-          <span style="margin-left: 8px;">%</span>
-        </el-form-item>
-        <el-form-item label="平台服务费率(%)">
-          <el-input-number
-            v-model="form.platformServiceRate"
-            :min="0"
-            :max="100"
-            :precision="2"
-            style="width: 200px;"
-          />
-          <span style="margin-left: 8px;">%</span>
-        </el-form-item>
-        <el-form-item label="启用状态">
-          <el-switch v-model="form.isActive" />
-        </el-form-item>
-      </el-form>
-      <span slot="footer">
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmit" :loading="submitLoading">保存</el-button>
-      </span>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { danshopProfitRuleList, danshopProfitRuleSave, danshopProfitRuleDelete } from '@/api/danshop'
-import { danshopCategoryList } from '@/api/danshop'
-
-export default {
-  name: 'DanShopProfitRule',
-  data() {
-    return {
-      list: [],
-      loading: false,
-      page: 1,
-      size: 20,
-      total: 0,
-      searchName: '',
-      statusLoading: null,
-      dialogVisible: false,
-      isEdit: false,
-      submitLoading: false,
-      form: {
-        id: null,
-        ruleName: '',
-        ruleType: 'global',
-        targetId: null,
-        targetName: '',
-        platformProfitRate: 0,
-        referrerL1Rate: 0,
-        referrerL2Rate: 0,
-        platformServiceRate: 0,
-        isActive: true
-      },
-      categoryOptions: []
-    }
-  },
-  mounted() {
-    this.loadList()
-    this.loadCategories()
-  },
-  methods: {
-    async loadList() {
-      this.loading = true
-      try {
-        const res = await danshopProfitRuleList({
-          page: this.page,
-          size: this.size,
-          ruleName: this.searchName || undefined
-        })
-        this.list = (res.data && res.data.list) || []
-        this.total = (res.data && res.data.total) || 0
-      } catch (e) {
-        this.$message.error('加载分润规则失败')
-      } finally {
-        this.loading = false
-      }
-    },
-    async loadCategories() {
-      try {
-        const res = await danshopCategoryList({ page: 1, size: 999 })
-        this.categoryOptions = (res.data && res.data.list) || []
-      } catch (e) {
-        // silently fail, categories are optional for the form
-      }
-    },
-    onPageChange(val) {
-      this.page = val
-      this.loadList()
-    },
-    handleSearch() {
-      this.page = 1
-      this.loadList()
-    },
-    onRuleTypeChange() {
-      this.form.targetId = null
-      this.form.targetName = ''
-    },
-    handleCreate() {
-      this.isEdit = false
-      this.form = {
-        id: null,
-        ruleName: '',
-        ruleType: 'global',
-        targetId: null,
-        targetName: '',
-        platformProfitRate: 0,
-        referrerL1Rate: 0,
-        referrerL2Rate: 0,
-        platformServiceRate: 0,
-        isActive: true
-      }
-      this.dialogVisible = true
-    },
-    handleEdit(row) {
-      this.isEdit = true
-      this.form = {
-        id: row.id,
-        ruleName: row.ruleName || '',
-        ruleType: row.ruleType || 'global',
-        targetId: row.targetId || null,
-        targetName: row.targetName || '',
-        platformProfitRate: row.platformProfitRate != null ? row.platformProfitRate : 0,
-        referrerL1Rate: row.referrerL1Rate != null ? row.referrerL1Rate : 0,
-        referrerL2Rate: row.referrerL2Rate != null ? row.referrerL2Rate : 0,
-        platformServiceRate: row.platformServiceRate != null ? row.platformServiceRate : 0,
-        isActive: row.isActive !== undefined ? row.isActive : true
-      }
-      this.dialogVisible = true
-    },
-    async handleSubmit() {
-      if (!this.form.ruleName.trim()) {
-        return this.$message.warning('请输入规则名称')
-      }
-      this.submitLoading = true
-      try {
-        await danshopProfitRuleSave({
-          id: this.form.id || undefined,
-          ruleName: this.form.ruleName,
-          ruleType: this.form.ruleType,
-          targetId: this.form.ruleType === 'global' ? undefined : (this.form.targetId || undefined),
-          platformProfitRate: this.form.platformProfitRate,
-          referrerL1Rate: this.form.referrerL1Rate,
-          referrerL2Rate: this.form.referrerL2Rate,
-          platformServiceRate: this.form.platformServiceRate,
-          isActive: this.form.isActive
-        })
-        this.$message.success(this.isEdit ? '规则已更新' : '规则已创建')
-        this.dialogVisible = false
-        this.loadList()
-      } catch (e) {
-        this.$message.error('保存失败')
-      } finally {
-        this.submitLoading = false
-      }
-    },
-    async handleDelete(row) {
-      try {
-        await this.$confirm(`确认删除规则「${row.ruleName}」?删除后不可恢复。`, '提示', {
-          confirmButtonText: '确定',
-          cancelButtonText: '取消',
-          type: 'warning'
-        })
-        await danshopProfitRuleDelete({ id: row.id })
-        this.$message.success('删除成功')
-        this.loadList()
-      } catch (e) {
-        if (e !== 'cancel') {
-          this.$message.error('删除失败')
-        }
-      }
-    },
-    async handleStatusChange(row, val) {
-      this.statusLoading = row.id
-      try {
-        await danshopProfitRuleSave({
-          id: row.id,
-          ruleName: row.ruleName,
-          ruleType: row.ruleType,
-          targetId: row.targetId || undefined,
-          platformProfitRate: row.platformProfitRate,
-          referrerL1Rate: row.referrerL1Rate,
-          referrerL2Rate: row.referrerL2Rate,
-          platformServiceRate: row.platformServiceRate,
-          isActive: val
-        })
-        this.$message.success(val ? '已启用' : '已停用')
-      } catch (e) {
-        row.isActive = !val
-        this.$message.error('状态更新失败')
-      } finally {
-        this.statusLoading = null
-      }
-    },
-    ruleTypeLabel(type) {
-      const map = { product_category: '商品分类', vendor_level: '供应商等级', global: '全局' }
-      return map[type] || type || '-'
-    },
-    ruleTypeTag(type) {
-      const map = { product_category: 'primary', vendor_level: 'warning', global: 'success' }
-      return map[type] || 'info'
-    },
-    formatTime(time) {
-      if (!time) return '-'
-      return new Date(time).toLocaleString()
-    }
-  }
-}
-</script>
-
-<style scoped>
-.profit-rule-config {
-  padding: 20px;
-}
-.header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20px;
-}
-.header h2 {
-  margin: 0;
-}
-.header-actions {
-  display: flex;
-  align-items: center;
-}
-</style>

+ 18 - 84
cfc-web/src/views/admin/OrderManage.vue

@@ -4,25 +4,14 @@
       <h2>订单管理</h2>
       <div class="filters">
         <el-radio-group v-model="sourceTab" size="small" @change="onSourceChange" style="margin-right: 10px;">
-          <el-radio-button label="cfc">平台订单</el-radio-button>
-          <el-radio-button label="danshop">DanShop订单</el-radio-button>
-        </el-radio-group>
+          </el-radio-group>
         <el-select v-model="statusFilter" placeholder="订单状态" @change="loadList" style="width: 150px;">
           <el-option label="全部" value="" />
-          <template v-if="sourceTab === 'cfc'">
-            <el-option label="待支付" value="pending" />
-            <el-option label="已支付" value="paid" />
-            <el-option label="已完成" value="completed" />
-            <el-option label="已取消" value="cancelled" />
-            <el-option label="已退款" value="refunded" />
-          </template>
-          <template v-if="sourceTab === 'danshop'">
-            <el-option label="待支付" value="0" />
-            <el-option label="待发货" value="1" />
-            <el-option label="待收货" value="2" />
-            <el-option label="已完成" value="3" />
-            <el-option label="已取消" value="4" />
-          </template>
+          <el-option label="待支付" value="pending" />
+          <el-option label="已支付" value="paid" />
+          <el-option label="已完成" value="completed" />
+          <el-option label="已取消" value="cancelled" />
+          <el-option label="已退款" value="refunded" />
         </el-select>
       </div>
     </div>
@@ -50,20 +39,17 @@
       </el-table-column>
       <el-table-column label="订单金额" width="100">
         <template slot-scope="{ row }">
-          <template v-if="sourceTab === 'danshop'">{{ formatPriceWithSymbol(row.totalAmount || 0) }}</template>
-          <template v-else>{{ formatPriceWithSymbol(row.amount || 0) }}</template>
+          {{ formatPriceWithSymbol(row.amount || 0) }}
         </template>
       </el-table-column>
       <el-table-column label="状态" width="100">
         <template slot-scope="{ row }">
-          <el-tag :type="sourceTab === 'danshop' ? danShopStatusType(row.status) : statusType(row.status)">
-            {{ sourceTab === 'danshop' ? danShopStatusLabel(row.status) : statusLabel(row.status) }}
-          </el-tag>
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
       <el-table-column label="支付方式" width="100">
         <template slot-scope="{ row }">
-          {{ sourceTab === 'danshop' ? danShopPayLabel(row.paymentMethod) : payMethodLabel(row.payMethod) }}
+          {{ payMethodLabel(row.payMethod) }}
         </template>
       </el-table-column>
       <el-table-column prop="createTime" label="下单时间" width="160">
@@ -74,8 +60,7 @@
       <el-table-column label="操作" width="200" fixed="right">
         <template slot-scope="{ row }">
           <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
-          <el-button v-if="sourceTab === 'danshop' && (row.status === 1)" type="primary" size="small" @click="showShipDialog(row)">发货</el-button>
-          <el-button v-if="sourceTab === 'danshop' && (row.status === 2)" type="success" size="small" @click="handleTrack(row)">物流</el-button>
+          
         </template>
       </el-table-column>
     </el-table>
@@ -95,27 +80,17 @@
         <el-descriptions-item label="订单ID">{{ detail.id }}</el-descriptions-item>
         <el-descriptions-item label="订单编号">{{ detail.orderNo || '-' }}</el-descriptions-item>
         <el-descriptions-item label="商品名称">{{ detail.productName || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="商品类型">{{ sourceTab === 'danshop' ? (detail.productType || '-') : typeLabel(detail.productType) }}</el-descriptions-item>
-        <el-descriptions-item label="订单金额">
-          <template v-if="sourceTab === 'danshop'">{{ formatPriceWithSymbol(detail.totalAmount || 0) }}</template>
-          <template v-else>{{ formatPriceWithSymbol(detail.amount || 0) }}</template>
-        </el-descriptions-item>
-        <el-descriptions-item label="实际支付">
-          <template v-if="sourceTab === 'danshop'">{{ formatPriceWithSymbol(detail.totalAmount || 0) }}</template>
-          <template v-else>{{ formatPriceWithSymbol(detail.paidAmount || 0) }}</template>
-        </el-descriptions-item>
+        <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
+        <el-descriptions-item label="订单金额">{{ formatPriceWithSymbol(detail.amount || 0) }}</el-descriptions-item>
+        <el-descriptions-item label="实际支付">{{ formatPriceWithSymbol(detail.paidAmount || 0) }}</el-descriptions-item>
         <el-descriptions-item label="买家昵称">{{ detail.buyerNickname || '-' }}</el-descriptions-item>
         <el-descriptions-item label="买家手机">{{ detail.buyerPhone || '-' }}</el-descriptions-item>
         <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="供应商手机">{{ detail.vendorPhone || '-' }}</el-descriptions-item>
         <el-descriptions-item label="订单状态">
-          <el-tag :type="sourceTab === 'danshop' ? danShopStatusType(detail.status) : statusType(detail.status)">
-            {{ sourceTab === 'danshop' ? danShopStatusLabel(detail.status) : statusLabel(detail.status) }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="支付方式">
-          {{ sourceTab === 'danshop' ? danShopPayLabel(detail.paymentMethod) : payMethodLabel(detail.payMethod) }}
+          <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
         </el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ payMethodLabel(detail.payMethod) }}</el-descriptions-item>
         <el-descriptions-item label="收货人">{{ detail.receiverName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="收货电话">{{ detail.receiverPhone || '-' }}</el-descriptions-item>
         <el-descriptions-item label="收货地址" :span="2">{{ detail.shippingAddress || detail.receiverAddress || '-' }}</el-descriptions-item>
@@ -147,7 +122,6 @@
 
 <script>
 import { getOrderList } from '@/api/admin'
-import { danshopOrderList, danshopLogisticsShip, danshopLogisticsQuery } from '@/api/danshop'
 
 export default {
   data() {
@@ -180,20 +154,9 @@ export default {
     async loadList() {
       this.loading = true
       try {
-        if (this.sourceTab === 'cfc') {
-          var res = await getOrderList({ status: this.statusFilter })
-          this.list = res.data || []
-          this.total = (res.data || []).length
-        } else {
-          var params = { page: this.page, size: this.size }
-          if (this.statusFilter !== '') {
-            params.status = parseInt(this.statusFilter)
-          }
-          var res2 = await danshopOrderList(params)
-          var data = res2.data || {}
-          this.list = data.records || data || []
-          this.total = data.total || this.list.length
-        }
+        var res = await getOrderList({ status: this.statusFilter })
+        this.list = res.data || []
+        this.total = (res.data || []).length
       } catch (e) {
         this.$message.error('加载失败')
       } finally {
@@ -212,22 +175,10 @@ export default {
       var map = { pending: '待支付', paid: '已支付', completed: '已完成', cancelled: '已取消', refunded: '已退款' }
       return map[status] || status
     },
-    danShopStatusType(status) {
-      var map = { 0: 'warning', 1: 'primary', 2: '', 3: 'success', 4: 'info', 5: 'danger', 6: 'danger' }
-      return map[status] || 'info'
-    },
-    danShopStatusLabel(status) {
-      var map = { 0: '待支付', 1: '待发货', 2: '待收货', 3: '已完成', 4: '已取消', 5: '退款中', 6: '已退款' }
-      return map[status] || status
-    },
     payMethodLabel(method) {
       var map = { wechat: '微信支付', balance: '余额支付', points: '积分兑换', test: '测试支付' }
       return map[method] || method || '-'
     },
-    danShopPayLabel(method) {
-      var map = { 1: '微信支付', 2: '余额支付' }
-      return map[method] || (method != null ? method : '-')
-    },
     typeLabel(type) {
       var map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
       return map[type] || type || '-'
@@ -252,11 +203,6 @@ export default {
       }
       this.shipLoading = true
       try {
-        await danshopLogisticsShip({
-          orderId: this.shipTarget.id,
-          company: this.shipForm.company,
-          trackingNo: this.shipForm.trackingNo
-        })
         this.$message.success('发货成功')
         this.shipDialogVisible = false
         this.loadList()
@@ -265,18 +211,6 @@ export default {
       } finally {
         this.shipLoading = false
       }
-    },
-    async handleTrack(row) {
-      try {
-        var res = await danshopLogisticsQuery({ orderId: row.id })
-        if (res.data) {
-          this.$alert(JSON.stringify(res.data, null, 2), '物流信息', { customClass: 'logistics-detail' })
-        } else {
-          this.$message.info('暂无物流信息')
-        }
-      } catch (e) {
-        this.$message.error('查询物流失败')
-      }
     }
   }
 }

+ 13 - 61
cfc-web/src/views/admin/ProductManage.vue

@@ -3,10 +3,6 @@
     <div class="header">
       <h2>商品管理</h2>
       <div class="filters">
-        <el-radio-group v-model="sourceTab" size="small" @change="onSourceChange" style="margin-right: 10px;">
-          <el-radio-button label="cfc">平台商品</el-radio-button>
-          <el-radio-button label="danshop">DanShop商品</el-radio-button>
-        </el-radio-group>
         <el-select v-model="statusFilter" placeholder="商品状态" @change="loadList" style="width: 140px;">
           <el-option label="全部" value="" />
           <el-option label="待审核" value="pending" />
@@ -14,16 +10,12 @@
           <el-option label="已拒绝" value="rejected" />
           <el-option label="已下架" value="shelved" />
         </el-select>
-        <el-select v-if="sourceTab === 'cfc'" v-model="typeFilter" placeholder="商品类型" @change="loadList" style="width: 140px;">
+        <el-select v-model="typeFilter" placeholder="商品类型" @change="loadList" style="width: 140px;">
           <el-option label="全部类型" value="" />
           <el-option label="实物商品" value="physical" />
           <el-option label="虚拟商品" value="virtual" />
           <el-option label="优惠券" value="coupon" />
         </el-select>
-        <el-select v-if="sourceTab === 'danshop'" v-model="danshopCategoryFilter" placeholder="DanShop分类" @change="loadList" style="width: 140px;">
-          <el-option label="全部分类" value="" />
-          <el-option v-for="cat in danshopCategories" :key="cat.id" :label="cat.name" :value="cat.id" />
-        </el-select>
       </div>
     </div>
 
@@ -38,7 +30,7 @@
       <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip />
       <el-table-column label="类型" width="90">
         <template slot-scope="{ row }">
-          {{ sourceTab === 'danshop' ? danShopTypeLabel(row.productType) : typeLabel(row.productType) }}
+          {{ typeLabel(row.productType) }}
         </template>
       </el-table-column>
       <el-table-column label="售价(元)" width="90">
@@ -64,16 +56,10 @@
       </el-table-column>
       <el-table-column label="操作" width="220" fixed="right">
         <template slot-scope="{ row }">
-          <template v-if="sourceTab === 'cfc'">
-            <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleReview(row, 'approve')">通过</el-button>
-            <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="showRejectDialog(row)">拒绝</el-button>
-            <el-button v-if="row.status === 'approved'" type="warning" size="small" @click="handleShelve(row, false)">下架</el-button>
-            <el-button v-if="row.status === 'shelved'" type="primary" size="small" @click="handleShelve(row, true)">上架</el-button>
-          </template>
-          <template v-if="sourceTab === 'danshop'">
-            <el-button v-if="row.status === 0 || row.status === 'pending'" type="success" size="small" @click="handleDanShopShelve(row, true)">上架</el-button>
-            <el-button v-if="row.status === 1 || row.status === 'approved'" type="warning" size="small" @click="handleDanShopShelve(row, false)">下架</el-button>
-          </template>
+          <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleReview(row, 'approve')">通过</el-button>
+          <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="showRejectDialog(row)">拒绝</el-button>
+          <el-button v-if="row.status === 'approved'" type="warning" size="small" @click="handleShelve(row, false)">下架</el-button>
+          <el-button v-if="row.status === 'shelved'" type="primary" size="small" @click="handleShelve(row, true)">上架</el-button>
           <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
         </template>
       </el-table-column>
@@ -100,7 +86,7 @@
     <el-dialog title="商品详情" :visible.sync="detailDialogVisible" width="600px">
       <el-descriptions :column="2" border v-if="detail">
         <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
-        <el-descriptions-item label="商品类型">{{ sourceTab === 'danshop' ? danShopTypeLabel(detail.productType) : typeLabel(detail.productType) }}</el-descriptions-item>
+        <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
         <el-descriptions-item label="售价">{{ formatPriceWithSymbol(detail.price || 0) }}</el-descriptions-item>
         <el-descriptions-item label="会员价">{{ detail.memberPrice ? formatPriceWithSymbol(detail.memberPrice) : '-' }}</el-descriptions-item>
         <el-descriptions-item label="库存">{{ detail.stock }}</el-descriptions-item>
@@ -120,12 +106,12 @@
 
 <script>
 import { getProductList, reviewProduct, shelveProduct } from '@/api/admin'
-import { danshopCategoryList, danshopProductList } from '@/api/danshop'
+
 
 export default {
   data() {
     return {
-      sourceTab: 'cfc',
+
       list: [],
       loading: false,
       page: 1,
@@ -133,8 +119,7 @@ export default {
       total: 0,
       statusFilter: '',
       typeFilter: '',
-      danshopCategoryFilter: '',
-      danshopCategories: [],
+
       rejectDialogVisible: false,
       rejectTarget: null,
       rejectReason: '',
@@ -144,46 +129,20 @@ export default {
   },
   mounted() {
     this.loadList()
-    this.loadDanShopCategories()
   },
   methods: {
-    onSourceChange() {
-      this.page = 1
-      this.list = []
-      this.statusFilter = ''
-      this.typeFilter = ''
-      this.danshopCategoryFilter = ''
-      this.loadList()
-    },
     async loadList() {
       this.loading = true
       try {
-        if (this.sourceTab === 'cfc') {
-          var res = await getProductList({ status: this.statusFilter })
-          this.list = res.data || []
-          this.total = (res.data || []).length
-        } else {
-          var params = { page: this.page, size: this.size }
-          if (this.danshopCategoryFilter) {
-            params.categoryId = this.danshopCategoryFilter
-          }
-          var res2 = await danshopProductList(params)
-          var data = res2.data || {}
-          this.list = data.records || data || []
-          this.total = data.total || this.list.length
-        }
+        var res = await getProductList({ status: this.statusFilter })
+        this.list = res.data || []
+        this.total = (res.data || []).length
       } catch (e) {
         this.$message.error('加载失败')
       } finally {
         this.loading = false
       }
     },
-    async loadDanShopCategories() {
-      try {
-        var res = await danshopCategoryList({})
-        this.danshopCategories = res.data || []
-      } catch (e) {}
-    },
     onPageChange(val) {
       this.page = val
       this.loadList()
@@ -202,10 +161,6 @@ export default {
       var map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
       return map[type] || type
     },
-    danShopTypeLabel(type) {
-      var map = { physical: '实物', digital: '数字', service: '服务', virtual: '虚拟' }
-      return map[type] || type
-    },
     formatTime(time) {
       if (!time) return '-'
       return new Date(time).toLocaleString()
@@ -248,9 +203,6 @@ export default {
         if (e !== 'cancel') this.$message.error('操作失败')
       }
     },
-    async handleDanShopShelve(row, shelve) {
-      this.$message.info('DanShop商品上下架请前往DanShop管理端操作')
-    },
     viewDetail(row) {
       this.detail = row
       this.detailDialogVisible = true

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä