Ver código fonte

厂商管理
总后台数据总览优化
一键代发
财务总账
多店铺购物车,下单

lk 1 dia atrás
pai
commit
c69e65bb05
67 arquivos alterados com 3612 adições e 133 exclusões
  1. 189 0
      adminui/src/views/admin/deviceManufacturer/index.vue
  2. 162 0
      adminui/src/views/admin/deviceType/index.vue
  3. 88 0
      adminui/src/views/admin/dropShipping/index.vue
  4. 184 0
      adminui/src/views/admin/finance/ledger.vue
  5. 258 0
      adminui/src/views/admin/resourceAuth/index.vue
  6. 192 0
      adminui/src/views/admin/supplier/index.vue
  7. 203 0
      adminui/src/views/admin/tenantPackage/index.vue
  8. 8 0
      agentui/src/api/tenant.js
  9. 30 0
      agentui/src/router/index.js
  10. 88 0
      agentui/src/views/notice/index.vue
  11. 196 0
      agentui/src/views/purchaseSplit/index.vue
  12. 38 4
      agentui/src/views/tenant/index.vue
  13. 40 0
      java/fs-admin/src/main/java/com/fs/admin/controller/IndexController.java
  14. 54 0
      java/fs-admin/src/main/java/com/fs/admin/controller/device/DeviceManufacturerAdminController.java
  15. 51 0
      java/fs-admin/src/main/java/com/fs/admin/controller/device/DeviceTypeAdminController.java
  16. 37 0
      java/fs-admin/src/main/java/com/fs/admin/controller/finance/FinancialLedgerController.java
  17. 59 0
      java/fs-admin/src/main/java/com/fs/admin/controller/platform/ResourceAuthController.java
  18. 32 0
      java/fs-admin/src/main/java/com/fs/admin/controller/store/DropShippingController.java
  19. 51 0
      java/fs-admin/src/main/java/com/fs/admin/controller/store/SupplierAdminController.java
  20. 48 0
      java/fs-admin/src/main/java/com/fs/admin/controller/tenant/TenantPackageController.java
  21. 9 0
      java/fs-admin/src/main/java/com/fs/admin/vo/AdminDashboardVO.java
  22. 30 0
      java/fs-agent/src/main/java/com/fs/proxy/controller/ProxyController.java
  23. 52 0
      java/fs-agent/src/main/java/com/fs/proxy/controller/ProxyPurchaseSplitController.java
  24. 1 1
      java/fs-saas-admin/pom.xml
  25. 5 0
      java/fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceLogsMapper.java
  26. 6 0
      java/fs-service/src/main/java/com/fs/device/service/IDeviceManufacturerService.java
  27. 13 0
      java/fs-service/src/main/java/com/fs/device/service/impl/DeviceManufacturerServiceImpl.java
  28. 26 0
      java/fs-service/src/main/java/com/fs/finance/domain/FinancialLedger.java
  29. 12 0
      java/fs-service/src/main/java/com/fs/finance/mapper/FinancialLedgerMapper.java
  30. 17 0
      java/fs-service/src/main/java/com/fs/finance/service/IFinancialLedgerService.java
  31. 45 0
      java/fs-service/src/main/java/com/fs/finance/service/impl/FinancialLedgerServiceImpl.java
  32. 3 0
      java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java
  33. 12 0
      java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  34. 21 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreConfirmMultiStoreParam.java
  35. 19 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreMultiCartCountParam.java
  36. 9 19
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreMultiStoreOrderCreateParam.java
  37. 31 0
      java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderMultiStoreComputedParam.java
  38. 12 0
      java/fs-service/src/main/java/com/fs/hisStore/service/IDropShippingService.java
  39. 11 12
      java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCartScrmService.java
  40. 16 0
      java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  41. 26 0
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/DropShippingServiceImpl.java
  42. 6 4
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java
  43. 409 69
      java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  44. 1 1
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartGroupVO.java
  45. 1 1
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartStoreVO.java
  46. 60 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreMultiStoreComputedVO.java
  47. 39 0
      java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreMultiStoreConfirmVO.java
  48. 25 0
      java/fs-service/src/main/java/com/fs/platform/domain/ResourceAuth.java
  49. 7 0
      java/fs-service/src/main/java/com/fs/platform/mapper/ResourceAuthMapper.java
  50. 13 0
      java/fs-service/src/main/java/com/fs/platform/service/IResourceAuthService.java
  51. 38 0
      java/fs-service/src/main/java/com/fs/platform/service/impl/ResourceAuthServiceImpl.java
  52. 60 0
      java/fs-service/src/main/java/com/fs/proxy/domain/ProxyPurchaseSplit.java
  53. 9 0
      java/fs-service/src/main/java/com/fs/proxy/mapper/ProxyPurchaseSplitMapper.java
  54. 21 0
      java/fs-service/src/main/java/com/fs/proxy/service/IProxyPurchaseSplitService.java
  55. 70 0
      java/fs-service/src/main/java/com/fs/proxy/service/impl/ProxyPurchaseSplitServiceImpl.java
  56. 7 0
      java/fs-service/src/main/java/com/fs/tenant/domain/TenantInfo.java
  57. 27 0
      java/fs-service/src/main/java/com/fs/tenant/domain/TenantPackage.java
  58. 7 0
      java/fs-service/src/main/java/com/fs/tenant/mapper/TenantPackageMapper.java
  59. 11 0
      java/fs-service/src/main/java/com/fs/tenant/service/ITenantPackageService.java
  60. 27 0
      java/fs-service/src/main/java/com/fs/tenant/service/impl/TenantPackageServiceImpl.java
  61. 33 0
      java/fs-service/src/main/java/com/fs/tenant/task/TenantExpireCheckTask.java
  62. 80 8
      java/fs-service/src/main/resources/db/tenant-initData.sql
  63. 212 1
      java/fs-service/src/main/resources/db/tenant-initTable.sql
  64. 11 0
      java/fs-service/src/main/resources/mapper/hisStore/FsStoreCartScrmMapper.xml
  65. 15 2
      java/fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  66. 21 10
      java/fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java
  67. 18 1
      java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

+ 189 - 0
adminui/src/views/admin/deviceManufacturer/index.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="厂商名称" prop="name">
+          <el-input v-model="queryParams.name" placeholder="请输入厂商名称" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8 list-toolbar">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openDialog(null)" v-hasPermi="['admin:deviceManufacturer:add']">新增厂商</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="厂商名称" prop="name" min-width="140" show-overflow-tooltip />
+      <el-table-column label="联系人" prop="contactPerson" min-width="90" align="center" />
+      <el-table-column label="联系电话" prop="contactPhone" min-width="110" align="center" />
+      <el-table-column label="主营类别" prop="mainCategory" min-width="110" align="center" />
+      <el-table-column label="评级" prop="rating" min-width="80" align="center" />
+      <el-table-column label="状态" align="center" min-width="70">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" min-width="150" align="center" />
+      <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(scope.row)" v-hasPermi="['admin:deviceManufacturer:edit']">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['admin:deviceManufacturer:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="560px" append-to-body @closed="reset">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="厂商名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入厂商名称" />
+        </el-form-item>
+        <el-form-item label="统一信用代码" prop="creditCode">
+          <el-input v-model="form.creditCode" placeholder="请输入统一信用代码" />
+        </el-form-item>
+        <el-form-item label="联系人" prop="contactPerson">
+          <el-input v-model="form.contactPerson" placeholder="请输入联系人姓名" />
+        </el-form-item>
+        <el-form-item label="联系电话" prop="contactPhone">
+          <el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
+        </el-form-item>
+        <el-form-item label="邮箱" prop="email">
+          <el-input v-model="form.email" placeholder="请输入邮箱" />
+        </el-form-item>
+        <el-form-item label="联系地址" prop="address">
+          <el-input v-model="form.address" placeholder="请输入联系地址" />
+        </el-form-item>
+        <el-form-item label="主营类别" prop="mainCategory">
+          <el-input v-model="form.mainCategory" placeholder="请输入主营类别" />
+        </el-form-item>
+        <el-form-item label="评级" prop="rating">
+          <el-input v-model="form.rating" placeholder="请输入评级" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listManufacturer, getManufacturer, addManufacturer, updateManufacturer, delManufacturer } from '@/api/admin/deviceManufacturer'
+
+export default {
+  name: 'AdminDeviceManufacturer',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, name: null, status: null },
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: {
+        id: null, name: '', creditCode: '', contactPerson: '', contactPhone: '',
+        email: '', address: '', mainCategory: '', rating: '', status: 1
+      },
+      rules: {
+        name: [{ required: true, message: '请输入厂商名称', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listManufacturer(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑厂商'
+        getManufacturer(row.id).then(r => {
+          this.form = { ...this.form, ...r.data }
+          this.dialogVisible = true
+        })
+      } else {
+        this.formTitle = '新增厂商'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.id ? updateManufacturer(this.form) : addManufacturer(this.form)
+        req.then(() => {
+          this.$message.success(this.form.id ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.getList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除厂商 "${row.name}"?`, '提示', { type: 'warning' }).then(() => {
+        delManufacturer(row.id).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      })
+    },
+    reset() {
+      this.form = {
+        id: null, name: '', creditCode: '', contactPerson: '', contactPhone: '',
+        email: '', address: '', mainCategory: '', rating: '', status: 1
+      }
+      if (this.$refs.form) this.$refs.form.resetFields()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 162 - 0
adminui/src/views/admin/deviceType/index.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="设备类型" prop="typeName">
+          <el-input v-model="queryParams.typeName" placeholder="请输入设备类型" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8 list-toolbar">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openDialog(null)" v-hasPermi="['admin:deviceType:add']">新增设备类型</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="设备类型名称" prop="typeName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="父级ID" prop="parentId" min-width="80" align="center" />
+      <el-table-column label="状态" align="center" min-width="70">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" min-width="150" align="center" />
+      <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(scope.row)" v-hasPermi="['admin:deviceType:edit']">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['admin:deviceType:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="500px" append-to-body @closed="reset">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="设备类型名称" prop="typeName">
+          <el-input v-model="form.typeName" placeholder="请输入设备类型名称" />
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listDeviceType, getDeviceType, addDeviceType, updateDeviceType, delDeviceType } from '@/api/admin/deviceType'
+
+export default {
+  name: 'AdminDeviceType',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, typeName: null, status: null },
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: { id: null, typeName: '', description: '', status: 1 },
+      rules: {
+        typeName: [{ required: true, message: '请输入设备类型名称', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listDeviceType(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑设备类型'
+        getDeviceType(row.id).then(r => {
+          this.form = { ...this.form, ...r.data }
+          this.dialogVisible = true
+        })
+      } else {
+        this.formTitle = '新增设备类型'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.id ? updateDeviceType(this.form) : addDeviceType(this.form)
+        req.then(() => {
+          this.$message.success(this.form.id ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.getList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除设备类型 "${row.typeName}"?`, '提示', { type: 'warning' }).then(() => {
+        delDeviceType(row.id).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      })
+    },
+    reset() {
+      this.form = { id: null, typeName: '', description: '', status: 1 }
+      if (this.$refs.form) this.$refs.form.resetFields()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 88 - 0
adminui/src/views/admin/dropShipping/index.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="待处理" value="PENDING" />
+            <el-option label="配送中" value="SHIPPING" />
+            <el-option label="已完成" value="COMPLETED" />
+            <el-option label="已取消" value="CANCELLED" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="订单编码" prop="orderCode" min-width="140" show-overflow-tooltip />
+      <el-table-column label="门店名称" prop="storeName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="总价" prop="totalPrice" min-width="90" align="center">
+        <template slot-scope="scope">
+          <span style="color:#fa8c16;font-weight:bold">¥{{ scope.row.totalPrice || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" min-width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 'PENDING'" type="warning" size="mini">待处理</el-tag>
+          <el-tag v-else-if="scope.row.status === 'SHIPPING'" type="primary" size="mini">配送中</el-tag>
+          <el-tag v-else-if="scope.row.status === 'COMPLETED'" type="success" size="mini">已完成</el-tag>
+          <el-tag v-else-if="scope.row.status === 'CANCELLED'" type="danger" size="mini">已取消</el-tag>
+          <el-tag v-else type="info" size="mini">{{ scope.row.status }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" min-width="150" align="center" />
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+  </div>
+</template>
+
+<script>
+import { listDropShipping } from '@/api/admin/dropShipping'
+
+export default {
+  name: 'AdminDropShipping',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, status: null }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listDropShipping(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 184 - 0
adminui/src/views/admin/finance/ledger.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 汇总卡片 ===== -->
+    <el-row :gutter="16" class="mb16">
+      <el-col :span="8">
+        <el-card shadow="hover" class="summary-card">
+          <div class="card-inner">
+            <div class="card-icon" style="background:#e6f7ff"><i class="el-icon-wallet"></i></div>
+            <div class="card-info">
+              <span class="card-value">{{ totalAmountText }}</span>
+              <span class="card-label">总金额</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover" class="summary-card">
+          <div class="card-inner">
+            <div class="card-icon" style="background:#fff7e6"><i class="el-icon-s-order"></i></div>
+            <div class="card-info">
+              <span class="card-value">{{ distAmountText }}</span>
+              <span class="card-label">配送订单(DISTRIBUTION)</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover" class="summary-card">
+          <div class="card-inner">
+            <div class="card-icon" style="background:#f9f0ff"><i class="el-icon-service"></i></div>
+            <div class="card-info">
+              <div>
+                <span class="card-sub-value">{{ stockAmountText }}</span>
+                <span class="card-sub-label">STOCK</span>
+              </div>
+              <div>
+                <span class="card-sub-value">{{ serviceAmountText }}</span>
+                <span class="card-sub-label">SERVICE</span>
+              </div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="订单类型" prop="orderType">
+          <el-select v-model="queryParams.orderType" placeholder="全部" clearable style="width:160px">
+            <el-option label="配送" value="DISTRIBUTION" />
+            <el-option label="入库" value="STOCK" />
+            <el-option label="服务" value="SERVICE" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="订单类型" align="center" min-width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.orderType === 'DISTRIBUTION'" type="warning" size="mini">配送</el-tag>
+          <el-tag v-else-if="scope.row.orderType === 'STOCK'" size="mini">入库</el-tag>
+          <el-tag v-else-if="scope.row.orderType === 'SERVICE'" type="success" size="mini">服务</el-tag>
+          <el-tag v-else type="info" size="mini">{{ scope.row.orderType }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="订单编码" prop="orderCode" min-width="140" show-overflow-tooltip />
+      <el-table-column label="来源账户" prop="fromAccount" min-width="120" align="center" />
+      <el-table-column label="目标账户" prop="toAccount" min-width="120" align="center" />
+      <el-table-column label="金额" prop="amount" min-width="100" align="center">
+        <template slot-scope="scope">
+          <span style="color:#fa8c16;font-weight:bold">¥{{ scope.row.amount || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手续费" prop="fee" min-width="90" align="center">
+        <template slot-scope="scope">
+          ¥{{ scope.row.fee || 0 }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" min-width="70">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" min-width="150" align="center" />
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+  </div>
+</template>
+
+<script>
+import { listLedger, getLedgerSummary } from '@/api/admin/finance'
+
+export default {
+  name: 'AdminFinanceLedger',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, orderType: null, status: null },
+      summary: {}
+    }
+  },
+  computed: {
+    totalAmountText() {
+      const v = parseFloat(this.summary.totalAmount) || 0
+      return '¥' + v.toFixed(2)
+    },
+    distAmountText() {
+      const v = parseFloat(this.summary.distAmount) || 0
+      return '¥' + v.toFixed(2)
+    },
+    stockAmountText() {
+      const v = parseFloat(this.summary.stockAmount) || 0
+      return '¥' + v.toFixed(2)
+    },
+    serviceAmountText() {
+      const v = parseFloat(this.summary.serviceAmount) || 0
+      return '¥' + v.toFixed(2)
+    }
+  },
+  created() {
+    this.loadSummary()
+    this.getList()
+  },
+  methods: {
+    loadSummary() {
+      getLedgerSummary().then(r => {
+        this.summary = r.data || {}
+      })
+    },
+    getList() {
+      this.loading = true
+      listLedger(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+.summary-card { cursor: default; }
+.card-inner { display: flex; align-items: center; }
+.card-icon {
+  width: 46px; height: 46px; border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+  margin-right: 14px; font-size: 22px; color: #fff;
+  flex-shrink: 0;
+}
+.card-value { font-size: 22px; font-weight: bold; color: #333; display: block; }
+.card-label { font-size: 12px; color: #999; }
+.card-sub-value { font-size: 16px; font-weight: bold; color: #333; }
+.card-sub-label { font-size: 12px; color: #999; margin-left: 6px; margin-right: 16px; }
+</style>

+ 258 - 0
adminui/src/views/admin/resourceAuth/index.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="租户ID" prop="tenantId">
+          <el-input v-model="queryParams.tenantId" placeholder="请输入租户ID" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="资源类型" prop="resourceType">
+          <el-select v-model="queryParams.resourceType" placeholder="全部" clearable style="width:140px">
+            <el-option label="课程" value="COURSE" />
+            <el-option label="直播" value="LIVE" />
+            <el-option label="商品" value="PRODUCT" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8 list-toolbar">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openDialog(null)" v-hasPermi="['admin:resourceAuth:add']">新增授权</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" icon="el-icon-s-operation" size="mini" @click="openBatchDialog" v-hasPermi="['admin:resourceAuth:add']">批量添加</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="租户ID" prop="tenantId" min-width="80" align="center" />
+      <el-table-column label="资源类型" align="center" min-width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.resourceType === 'COURSE'" size="mini">课程</el-tag>
+          <el-tag v-else-if="scope.row.resourceType === 'LIVE'" type="success" size="mini">直播</el-tag>
+          <el-tag v-else-if="scope.row.resourceType === 'PRODUCT'" type="warning" size="mini">商品</el-tag>
+          <el-tag v-else type="info" size="mini">{{ scope.row.resourceType }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="资源ID" prop="resourceId" min-width="80" align="center" />
+      <el-table-column label="资源名称" prop="resourceName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="授权开始时间" prop="authStartTime" min-width="150" align="center" />
+      <el-table-column label="授权结束时间" prop="authEndTime" min-width="150" align="center" />
+      <el-table-column label="状态" align="center" min-width="70">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(scope.row)" v-hasPermi="['admin:resourceAuth:edit']">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['admin:resourceAuth:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="520px" append-to-body @closed="reset">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="租户ID" prop="tenantId">
+          <el-input v-model="form.tenantId" placeholder="请输入租户ID" />
+        </el-form-item>
+        <el-form-item label="资源类型" prop="resourceType">
+          <el-select v-model="form.resourceType" placeholder="请选择资源类型" style="width:100%">
+            <el-option label="课程" value="COURSE" />
+            <el-option label="直播" value="LIVE" />
+            <el-option label="商品" value="PRODUCT" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="资源ID" prop="resourceId">
+          <el-input v-model="form.resourceId" placeholder="请输入资源ID" />
+        </el-form-item>
+        <el-form-item label="资源名称" prop="resourceName">
+          <el-input v-model="form.resourceName" placeholder="请输入资源名称" />
+        </el-form-item>
+        <el-form-item label="授权开始时间" prop="authStartTime">
+          <el-date-picker v-model="form.authStartTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择授权开始时间" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="授权结束时间" prop="authEndTime">
+          <el-date-picker v-model="form.authEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择授权结束时间" style="width:100%" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- ===== 批量添加弹窗 ===== -->
+    <el-dialog title="批量添加授权" :visible.sync="batchDialogVisible" width="520px" append-to-body @closed="resetBatch">
+      <el-form ref="batchForm" :model="batchForm" :rules="batchRules" label-width="100px">
+        <el-form-item label="租户ID" prop="tenantId">
+          <el-input v-model="batchForm.tenantId" placeholder="请输入租户ID" />
+        </el-form-item>
+        <el-form-item label="资源类型" prop="resourceType">
+          <el-select v-model="batchForm.resourceType" placeholder="请选择资源类型" style="width:100%">
+            <el-option label="课程" value="COURSE" />
+            <el-option label="直播" value="LIVE" />
+            <el-option label="商品" value="PRODUCT" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="资源ID列表" prop="resourceIds">
+          <el-input v-model="batchForm.resourceIds" type="textarea" :rows="4" placeholder="请输入资源ID,多个用逗号分隔" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="batchDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="batchSubmitting" @click="submitBatchForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listResourceAuth, addResourceAuth, batchAddResourceAuth, updateResourceAuth, delResourceAuth } from '@/api/admin/resourceAuth'
+
+export default {
+  name: 'AdminResourceAuth',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, tenantId: null, resourceType: null, status: null },
+      // 单条编辑
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: {
+        id: null, tenantId: null, resourceType: '', resourceId: null,
+        resourceName: '', authStartTime: null, authEndTime: null
+      },
+      rules: {
+        tenantId: [{ required: true, message: '请输入租户ID', trigger: 'blur' }],
+        resourceType: [{ required: true, message: '请选择资源类型', trigger: 'change' }],
+        resourceId: [{ required: true, message: '请输入资源ID', trigger: 'blur' }]
+      },
+      // 批量添加
+      batchDialogVisible: false,
+      batchSubmitting: false,
+      batchForm: { tenantId: null, resourceType: '', resourceIds: '' },
+      batchRules: {
+        tenantId: [{ required: true, message: '请输入租户ID', trigger: 'blur' }],
+        resourceType: [{ required: true, message: '请选择资源类型', trigger: 'change' }],
+        resourceIds: [{ required: true, message: '请输入资源ID', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listResourceAuth(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    // ======== 单条操作 ========
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑授权'
+        this.form = { ...this.form, ...row }
+        this.dialogVisible = true
+      } else {
+        this.formTitle = '新增授权'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.id ? updateResourceAuth(this.form) : addResourceAuth(this.form)
+        req.then(() => {
+          this.$message.success(this.form.id ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.getList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除该授权记录?`, '提示', { type: 'warning' }).then(() => {
+        delResourceAuth(row.id).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      })
+    },
+    reset() {
+      this.form = {
+        id: null, tenantId: null, resourceType: '', resourceId: null,
+        resourceName: '', authStartTime: null, authEndTime: null
+      }
+      if (this.$refs.form) this.$refs.form.resetFields()
+    },
+    // ======== 批量操作 ========
+    openBatchDialog() {
+      this.resetBatch()
+      this.batchDialogVisible = true
+    },
+    submitBatchForm() {
+      this.$refs.batchForm.validate(valid => {
+        if (!valid) return
+        this.batchSubmitting = true
+        const resourceIds = this.batchForm.resourceIds.split(',').map(s => s.trim()).filter(Boolean)
+        const data = {
+          tenantId: this.batchForm.tenantId,
+          resourceType: this.batchForm.resourceType,
+          resourceIds: resourceIds
+        }
+        batchAddResourceAuth(data).then(() => {
+          this.$message.success('批量添加成功')
+          this.batchDialogVisible = false
+          this.getList()
+        }).finally(() => { this.batchSubmitting = false })
+      })
+    },
+    resetBatch() {
+      this.batchForm = { tenantId: null, resourceType: '', resourceIds: '' }
+      if (this.$refs.batchForm) this.$refs.batchForm.resetFields()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 192 - 0
adminui/src/views/admin/supplier/index.vue

@@ -0,0 +1,192 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="供应商名称" prop="name">
+          <el-input v-model="queryParams.name" placeholder="请输入供应商名称" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8 list-toolbar">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openDialog(null)" v-hasPermi="['admin:supplier:add']">新增供应商</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="编码" prop="code" min-width="100" align="center" />
+      <el-table-column label="供应商名称" prop="name" min-width="140" show-overflow-tooltip />
+      <el-table-column label="类型" prop="type" min-width="90" align="center" />
+      <el-table-column label="联系人" prop="contactName" min-width="90" align="center" />
+      <el-table-column label="联系电话" prop="contactPhone" min-width="110" align="center" />
+      <el-table-column label="状态" align="center" min-width="70">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" min-width="150" align="center" />
+      <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(scope.row)" v-hasPermi="['admin:supplier:edit']">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['admin:supplier:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="560px" append-to-body @closed="reset">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="供应商名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入供应商名称" />
+        </el-form-item>
+        <el-form-item label="编码" prop="code">
+          <el-input v-model="form.code" placeholder="请输入供应商编码" />
+        </el-form-item>
+        <el-form-item label="类型" prop="type">
+          <el-input v-model="form.type" placeholder="请输入供应商类型" />
+        </el-form-item>
+        <el-form-item label="等级" prop="level">
+          <el-input v-model="form.level" placeholder="请输入供应商等级" />
+        </el-form-item>
+        <el-form-item label="联系人" prop="contactName">
+          <el-input v-model="form.contactName" placeholder="请输入联系人姓名" />
+        </el-form-item>
+        <el-form-item label="联系电话" prop="contactPhone">
+          <el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
+        </el-form-item>
+        <el-form-item label="联系地址" prop="address">
+          <el-input v-model="form.address" placeholder="请输入联系地址" />
+        </el-form-item>
+        <el-form-item label="开户银行" prop="bankName">
+          <el-input v-model="form.bankName" placeholder="请输入开户银行" />
+        </el-form-item>
+        <el-form-item label="银行账号" prop="bankAccount">
+          <el-input v-model="form.bankAccount" placeholder="请输入银行账号" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listSupplier, getSupplier, addSupplier, updateSupplier, delSupplier } from '@/api/admin/supplier'
+
+export default {
+  name: 'AdminSupplier',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, name: null, status: null },
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: {
+        id: null, code: '', name: '', type: '', level: '', contactName: '', contactPhone: '',
+        address: '', bankName: '', bankAccount: '', status: 1
+      },
+      rules: {
+        name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listSupplier(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑供应商'
+        getSupplier(row.id).then(r => {
+          this.form = { ...this.form, ...r.data }
+          this.dialogVisible = true
+        })
+      } else {
+        this.formTitle = '新增供应商'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.id ? updateSupplier(this.form) : addSupplier(this.form)
+        req.then(() => {
+          this.$message.success(this.form.id ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.getList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除供应商 "${row.name}"?`, '提示', { type: 'warning' }).then(() => {
+        delSupplier(row.id).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      })
+    },
+    reset() {
+      this.form = {
+        id: null, code: '', name: '', type: '', level: '', contactName: '', contactPhone: '',
+        address: '', bankName: '', bankAccount: '', status: 1
+      }
+      if (this.$refs.form) this.$refs.form.resetFields()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 203 - 0
adminui/src/views/admin/tenantPackage/index.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="app-container">
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" class="list-search-form">
+        <el-form-item label="套餐名称" prop="packageName">
+          <el-input v-model="queryParams.packageName" placeholder="请输入套餐名称" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="禁用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8 list-toolbar">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="openDialog(null)" v-hasPermi="['admin:tenantPackage:add']">新增套餐</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- ===== 表格 ===== -->
+    <el-table border v-loading="loading" :data="list" size="small" style="width:100%">
+      <el-table-column label="ID" prop="id" min-width="60" align="center" />
+      <el-table-column label="套餐名称" prop="packageName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="套餐编码" prop="packageCode" min-width="120" align="center" />
+      <el-table-column label="价格" prop="price" min-width="90" align="center">
+        <template slot-scope="scope">
+          <span style="color:#fa8c16;font-weight:bold">¥{{ scope.row.price || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="有效期(天)" prop="durationDays" min-width="100" align="center" />
+      <el-table-column label="最大门店数" prop="maxStoreCount" min-width="100" align="center" />
+      <el-table-column label="状态" align="center" min-width="70">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="排序" prop="sort" min-width="70" align="center" />
+      <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(scope.row)" v-hasPermi="['admin:tenantPackage:edit']">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['admin:tenantPackage:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="560px" append-to-body @closed="reset">
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="套餐名称" prop="packageName">
+          <el-input v-model="form.packageName" placeholder="请输入套餐名称" />
+        </el-form-item>
+        <el-form-item label="套餐编码" prop="packageCode">
+          <el-input v-model="form.packageCode" placeholder="请输入套餐编码" />
+        </el-form-item>
+        <el-form-item label="价格" prop="price">
+          <el-input-number v-model="form.price" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="有效期(天)" prop="durationDays">
+          <el-input-number v-model="form.durationDays" :min="1" style="width:100%" placeholder="请输入有效天数" />
+        </el-form-item>
+        <el-form-item label="模块" prop="modules">
+          <el-input v-model="form.modules" placeholder="多个模块用逗号分隔" />
+        </el-form-item>
+        <el-form-item label="最大门店数" prop="maxStoreCount">
+          <el-input-number v-model="form.maxStoreCount" :min="0" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="最大员工数" prop="maxStaffCount">
+          <el-input-number v-model="form.maxStaffCount" :min="0" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="最大医生数" prop="maxDoctorCount">
+          <el-input-number v-model="form.maxDoctorCount" :min="0" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort">
+          <el-input-number v-model="form.sort" :min="0" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listTenantPackage, getTenantPackage, addTenantPackage, updateTenantPackage, delTenantPackage } from '@/api/admin/tenantPackage'
+
+export default {
+  name: 'AdminTenantPackage',
+  data() {
+    return {
+      showSearch: true,
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, packageName: null, status: null },
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: {
+        id: null, packageName: '', packageCode: '', price: 0, durationDays: null,
+        modules: '', maxStoreCount: null, maxStaffCount: null, maxDoctorCount: null,
+        status: 1, sort: 0, remark: ''
+      },
+      rules: {
+        packageName: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }],
+        packageCode: [{ required: true, message: '请输入套餐编码', trigger: 'blur' }],
+        price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listTenantPackage(this.queryParams).then(r => {
+        this.list = r.rows || []
+        this.total = r.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑套餐'
+        getTenantPackage(row.id).then(r => {
+          this.form = { ...this.form, ...r.data }
+          this.dialogVisible = true
+        })
+      } else {
+        this.formTitle = '新增套餐'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.id ? updateTenantPackage(this.form) : addTenantPackage(this.form)
+        req.then(() => {
+          this.$message.success(this.form.id ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.getList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除套餐 "${row.packageName}"?`, '提示', { type: 'warning' }).then(() => {
+        delTenantPackage(row.id).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      })
+    },
+    reset() {
+      this.form = {
+        id: null, packageName: '', packageCode: '', price: 0, durationDays: null,
+        modules: '', maxStoreCount: null, maxStaffCount: null, maxDoctorCount: null,
+        status: 1, sort: 0, remark: ''
+      }
+      if (this.$refs.form) this.$refs.form.resetFields()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 8 - 0
agentui/src/api/tenant.js

@@ -27,3 +27,11 @@ export function getTenantMenuTree(companyId, flag) {
 export function editTenantMenu(companyId, data) {
   return request({ url: `/proxy/tenants/${companyId}/menu/edit`, method: 'post', data })
 }
+
+export function getPackageList() {
+  return request({ url: '/proxy/tenants/packages', method: 'get' })
+}
+
+export function assignPackage(tenantId, packageId) {
+  return request({ url: `/proxy/tenants/${tenantId}/package`, method: 'put', params: { packageId } })
+}

+ 30 - 0
agentui/src/router/index.js

@@ -150,6 +150,36 @@ export const constantRoutes = [
       }
     ]
   },
+  {
+    path: '/purchaseSplit',
+    component: () => import('@/layout/index'),
+    redirect: '/purchaseSplit/list',
+    name: 'PurchaseSplit',
+    meta: { title: '进货分成', icon: 'el-icon-s-cooperation' },
+    children: [
+      {
+        path: 'list',
+        name: 'PurchaseSplitList',
+        component: () => import('@/views/purchaseSplit/index'),
+        meta: { title: '进货分成', icon: 'el-icon-s-cooperation' }
+      }
+    ]
+  },
+  {
+    path: '/notice',
+    component: () => import('@/layout/index'),
+    redirect: '/notice/list',
+    name: 'Notice',
+    meta: { title: '消息通知', icon: 'el-icon-bell' },
+    children: [
+      {
+        path: 'list',
+        name: 'NoticeList',
+        component: () => import('@/views/notice/index'),
+        meta: { title: '公告通知', icon: 'el-icon-bell' }
+      }
+    ]
+  },
   // 404 catch-all 必须放在最后,防止未匹配路由导致异常重定向
   { path: '*', redirect: '/404', hidden: true }
 ]

+ 88 - 0
agentui/src/views/notice/index.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="app-container">
+    <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
+      <el-table-column label="标题" prop="noticeTitle" min-width="200" show-overflow-tooltip />
+      <el-table-column label="类型" align="center" min-width="80">
+        <template slot-scope="{ row }">
+          <el-tag :type="row.noticeType === '1' ? 'primary' : 'warning'" size="small">
+            {{ row.noticeType === '1' ? '通知' : '公告' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="发布时间" prop="createTime" min-width="160" />
+      <el-table-column label="操作" align="center" min-width="100">
+        <template slot-scope="{ row }">
+          <el-button type="text" icon="el-icon-view" size="small" @click="handleDetail(row)">查看</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="detailTitle" :visible.sync="detailVisible" width="700px" append-to-body>
+      <div class="notice-detail" v-html="detailContent" />
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="detailVisible = false">关 闭</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getNoticeList, getNoticeDetail } from '@/api/notice'
+
+export default {
+  name: 'Notice',
+  data() {
+    return {
+      loading: false,
+      total: 0,
+      tableData: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10
+      },
+      detailVisible: false,
+      detailTitle: '',
+      detailContent: ''
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      getNoticeList(this.queryParams).then(res => {
+        this.tableData = res.rows
+        this.total = res.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleDetail(row) {
+      this.detailTitle = row.noticeTitle
+      getNoticeDetail(row.noticeId).then(res => {
+        this.detailContent = (res.data && res.data.noticeContent) || ''
+        this.detailVisible = true
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.notice-detail {
+  max-height: 400px;
+  overflow-y: auto;
+  padding: 10px 0;
+  line-height: 1.8;
+}
+</style>

+ 196 - 0
agentui/src/views/purchaseSplit/index.vue

@@ -0,0 +1,196 @@
+<template>
+  <div class="app-container">
+    <el-card class="filter-container" shadow="never">
+      <el-form :inline="true" :model="queryParams" size="small">
+        <el-form-item label="租户名称">
+          <el-input v-model="queryParams.tenantName" placeholder="请输入租户名称" clearable style="width: 180px" />
+        </el-form-item>
+        <el-form-item label="订单号">
+          <el-input v-model="queryParams.orderCode" placeholder="请输入订单号" clearable style="width: 180px" />
+        </el-form-item>
+        <el-form-item label="结算状态">
+          <el-select v-model="queryParams.settleStatus" placeholder="全部" clearable style="width: 120px">
+            <el-option label="待结算" :value="0" />
+            <el-option label="已结算" :value="1" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="月份">
+          <el-date-picker
+            v-model="queryParams.month"
+            type="month"
+            placeholder="选择月份"
+            value-format="yyyy-MM"
+            clearable
+            style="width: 150px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card class="summary-card" shadow="never" style="margin-top: 16px;">
+      <div class="summary-row">
+        <div class="summary-item">
+          <div class="summary-value">{{ summary.totalSplitAmount || 0 }}</div>
+          <div class="summary-label">总分成金额(元)</div>
+        </div>
+        <div class="summary-item">
+          <div class="summary-value warn">{{ summary.totalCount || 0 }}</div>
+          <div class="summary-label">总记录数</div>
+        </div>
+        <div class="summary-item">
+          <div class="summary-value primary">{{ summary.pendingCount || 0 }}</div>
+          <div class="summary-label">待结算</div>
+        </div>
+        <div class="summary-item">
+          <div class="summary-value success">{{ summary.settledCount || 0 }}</div>
+          <div class="summary-label">已结算</div>
+        </div>
+      </div>
+    </el-card>
+
+    <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%; margin-top: 16px;">
+      <el-table-column label="租户名称" prop="tenantName" min-width="150" />
+      <el-table-column label="订单号" prop="orderCode" min-width="180" />
+      <el-table-column label="进货金额" prop="purchaseAmount" min-width="120">
+        <template slot-scope="{ row }">{{ row.purchaseAmount }} 元</template>
+      </el-table-column>
+      <el-table-column label="分成比例" prop="splitRatio" min-width="100">
+        <template slot-scope="{ row }">{{ row.splitRatio }}%</template>
+      </el-table-column>
+      <el-table-column label="分成金额" prop="splitAmount" min-width="120">
+        <template slot-scope="{ row }">
+          <span style="color: #409EFF; font-weight: bold;">{{ row.splitAmount }} 元</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="结算状态" min-width="100" align="center">
+        <template slot-scope="{ row }">
+          <el-tag :type="row.settleStatus === 1 ? 'success' : 'warning'" size="small">
+            {{ row.settleStatus === 1 ? '已结算' : '待结算' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="结算月份" prop="month" min-width="100" />
+      <el-table-column label="结算时间" prop="settleTime" min-width="160" />
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { getPurchaseSplitList, getPurchaseSplitSummary } from '@/api/purchaseSplit'
+
+export default {
+  name: 'PurchaseSplit',
+  data() {
+    return {
+      loading: false,
+      total: 0,
+      tableData: [],
+      summary: {},
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        tenantName: '',
+        orderCode: '',
+        settleStatus: null,
+        month: ''
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.getSummary()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      getPurchaseSplitList(this.queryParams).then(res => {
+        this.tableData = res.rows
+        this.total = res.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    getSummary() {
+      getPurchaseSplitSummary(this.queryParams.month || undefined).then(res => {
+        this.summary = res.data || {}
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+      this.getSummary()
+    },
+    resetQuery() {
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        tenantName: '',
+        orderCode: '',
+        settleStatus: null,
+        month: ''
+      }
+      this.getList()
+      this.getSummary()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.summary-card {
+  margin-bottom: 0;
+}
+
+.summary-row {
+  display: flex;
+  gap: 24px;
+}
+
+.summary-item {
+  flex: 1;
+  text-align: center;
+  padding: 12px 0;
+  border-right: 1px solid #ebeef5;
+}
+
+.summary-item:last-child {
+  border-right: none;
+}
+
+.summary-value {
+  font-size: 24px;
+  font-weight: bold;
+  color: #303133;
+}
+
+.summary-value.warn {
+  color: #e6a23c;
+}
+
+.summary-value.primary {
+  color: #409eff;
+}
+
+.summary-value.success {
+  color: #67c23a;
+}
+
+.summary-label {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+</style>

+ 38 - 4
agentui/src/views/tenant/index.vue

@@ -117,6 +117,18 @@
               </el-radio-group>
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="订阅套餐">
+              <el-select v-model="tenantForm.packageId" placeholder="请选择套餐" clearable style="width:100%">
+                <el-option
+                  v-for="pkg in packageList"
+                  :key="pkg.id"
+                  :label="pkg.packageName"
+                  :value="pkg.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
         </el-row>
       </el-form>
       <div slot="footer">
@@ -162,7 +174,7 @@
 </template>
 
 <script>
-import { getTenantList, getTenant, addTenant, updateTenant, toggleTenantStatus, getTenantMenuTree, editTenantMenu } from '@/api/tenant'
+import { getTenantList, getTenant, addTenant, updateTenant, toggleTenantStatus, getTenantMenuTree, editTenantMenu, getPackageList, assignPackage } from '@/api/tenant'
 
 export default {
   name: 'Tenant',
@@ -211,8 +223,10 @@ export default {
         accountNum: 1,
         status: 1,
         userName: '',
-        password: ''
+        password: '',
+        packageId: null
       },
+      packageList: [],
       rules: {
         tenantName: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
         contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
@@ -223,8 +237,14 @@ export default {
   },
   mounted() {
     this.loadTenantList()
+    this.loadPackageList()
   },
   methods: {
+    loadPackageList() {
+      getPackageList().then(res => {
+        this.packageList = res.data || []
+      })
+    },
     async loadTenantList() {
       this.loading = true
       try {
@@ -260,7 +280,8 @@ export default {
         accountNum: 1,
         status: 1,
         userName: '',
-        password: ''
+        password: '',
+        packageId: null
       }
       this.dialogVisible = true
       this.$nextTick(() => {
@@ -282,7 +303,8 @@ export default {
           expireTime: data.expireTime || null,
           companyNum: data.companyNum || 1,
           accountNum: data.accountNum || 1,
-          status: data.status != null ? data.status : 1
+          status: data.status != null ? data.status : 1,
+          packageId: data.packageId || null
         }
         this.dialogVisible = true
         this.$nextTick(() => {
@@ -295,10 +317,22 @@ export default {
         if (!valid) return
         this.submitting = true
         try {
+          let tenantId = null
           if (this.isEdit) {
             await updateTenant(this.tenantForm)
+            tenantId = this.tenantForm.id
           } else {
             await addTenant(this.tenantForm)
+            // 新增后需要重新查询获取ID
+            await this.loadTenantList()
+            // 新增场景下套餐在新创建的租户上分配有难度,仅编辑时分配
+            this.dialogVisible = false
+            this.$message.success('操作成功')
+            return
+          }
+          // 编辑模式下,如果有套餐选择则分配
+          if (tenantId && this.tenantForm.packageId != null) {
+            await assignPackage(tenantId, this.tenantForm.packageId)
           }
           this.dialogVisible = false
           this.loadTenantList()

+ 40 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/IndexController.java

@@ -5,6 +5,10 @@ import com.fs.admin.vo.AdminDashboardVO;
 import com.fs.billing.mapper.TenantWalletTxnMapper;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.mapper.CompanyVoiceLogsMapper;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.mapper.TenantInfoMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -14,6 +18,8 @@ import org.springframework.web.bind.annotation.RestController;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
 
 /**
  * 总后台首页看板
@@ -30,6 +36,12 @@ public class IndexController extends BaseController {
     @Autowired
     private TenantWalletTxnMapper tenantWalletTxnMapper;
 
+    @Autowired
+    private CompanyVoiceLogsMapper companyVoiceLogsMapper;
+
+    @Autowired
+    private CompanyVoiceRoboticCallLogCallphoneMapper companyVoiceRoboticCallLogCallphoneMapper;
+
     /**
      * 首页看板统计卡片数据
      * GET /admin/index/dashboard
@@ -38,6 +50,8 @@ public class IndexController extends BaseController {
     public AjaxResult dashboard() {
         LocalDateTime todayStart = LocalDate.now().atStartOfDay();
         LocalDateTime todayEnd = LocalDate.now().plusDays(1).atStartOfDay();
+        Date todayStartDate = Date.from(todayStart.atZone(ZoneId.systemDefault()).toInstant());
+        Date todayEndDate = Date.from(todayEnd.atZone(ZoneId.systemDefault()).toInstant());
 
         // 总租户数
         Integer totalTenants = tenantInfoMapper.selectCount(new LambdaQueryWrapper<>());
@@ -60,11 +74,37 @@ public class IndexController extends BaseController {
             totalRecharge = BigDecimal.ZERO;
         }
 
+        // 今日新增租户数
+        Integer todayNewTenants = (tenantInfoMapper.selectCount(
+                new LambdaQueryWrapper<TenantInfo>()
+                        .between(TenantInfo::getCreateTime, todayStartDate, todayEndDate)));
+
+        // 今日外呼次数(人工 + AI)
+        Long todayManualCalls = companyVoiceLogsMapper.countManualCallsToday(todayStart, todayEnd);
+        if (todayManualCalls == null) {
+            todayManualCalls = 0L;
+        }
+        Integer todayAiCalls = (companyVoiceRoboticCallLogCallphoneMapper.selectCount(
+                new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
+                        .between(CompanyVoiceRoboticCallLogCallphone::getRunTime, todayStartDate, todayEndDate)));
+        Long todayCallCount = todayManualCalls + todayAiCalls;
+
+        // 总外呼次数(人工 + AI)
+        Long totalManualCalls = companyVoiceLogsMapper.countManualCalls();
+        if (totalManualCalls == null) {
+            totalManualCalls = 0L;
+        }
+        Integer totalAiCalls = companyVoiceRoboticCallLogCallphoneMapper.selectCount(new LambdaQueryWrapper<>());
+        Long totalCallCount = totalManualCalls + totalAiCalls;
+
         AdminDashboardVO vo = new AdminDashboardVO();
         vo.setTotalTenants(totalTenants);
         vo.setTodayConsumption(todayConsumption);
         vo.setTotalConsumption(totalConsumption);
         vo.setTotalRecharge(totalRecharge);
+        vo.setTodayCallCount(todayCallCount);
+        vo.setTotalCallCount(totalCallCount);
+        vo.setTodayNewTenants(Long.valueOf(todayNewTenants));
 
         return AjaxResult.success(vo);
     }

+ 54 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/device/DeviceManufacturerAdminController.java

@@ -0,0 +1,54 @@
+package com.fs.admin.controller.device;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.device.domain.DeviceManufacturer;
+import com.fs.device.service.IDeviceManufacturerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/admin/deviceManufacturer")
+public class DeviceManufacturerAdminController extends BaseController {
+    @Autowired
+    private IDeviceManufacturerService deviceManufacturerService;
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceManufacturer:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DeviceManufacturer manufacturer) {
+        startPage();
+        return getDataTable(deviceManufacturerService.selectManufacturerList(manufacturer));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceManufacturer:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(deviceManufacturerService.getById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceManufacturer:add')")
+    @PostMapping
+    public AjaxResult add(@RequestBody DeviceManufacturer manufacturer) {
+        return toAjax(deviceManufacturerService.save(manufacturer));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceManufacturer:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody DeviceManufacturer manufacturer) {
+        return toAjax(deviceManufacturerService.updateById(manufacturer));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceManufacturer:remove')")
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(deviceManufacturerService.removeByIds(java.util.Arrays.asList(ids)));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceManufacturer:query')")
+    @GetMapping("/allActive")
+    public AjaxResult allActive() {
+        return AjaxResult.success(deviceManufacturerService.listActive());
+    }
+}

+ 51 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/device/DeviceTypeAdminController.java

@@ -0,0 +1,51 @@
+package com.fs.admin.controller.device;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.device.domain.DeviceType;
+import com.fs.device.service.IDeviceTypeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin/deviceType")
+public class DeviceTypeAdminController extends BaseController {
+    @Autowired
+    private IDeviceTypeService deviceTypeService;
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceType:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DeviceType deviceType) {
+        startPage();
+        List<DeviceType> list = deviceTypeService.selectDeviceTypeList(deviceType);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceType:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(deviceTypeService.selectDeviceTypeById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceType:add')")
+    @PostMapping
+    public AjaxResult add(@RequestBody DeviceType deviceType) {
+        return toAjax(deviceTypeService.insertDeviceType(deviceType));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceType:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody DeviceType deviceType) {
+        return toAjax(deviceTypeService.updateDeviceType(deviceType));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:deviceType:remove')")
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(deviceTypeService.deleteDeviceTypeByIds(ids));
+    }
+}

+ 37 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/finance/FinancialLedgerController.java

@@ -0,0 +1,37 @@
+package com.fs.admin.controller.finance;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.finance.domain.FinancialLedger;
+import com.fs.finance.service.IFinancialLedgerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/admin/finance")
+public class FinancialLedgerController extends BaseController {
+    @Autowired
+    private IFinancialLedgerService financialLedgerService;
+
+    @PreAuthorize("@ss.hasPermi('admin:finance:list')")
+    @GetMapping("/ledger/list")
+    public TableDataInfo ledgerList(FinancialLedger ledger) {
+        startPage();
+        return getDataTable(financialLedgerService.selectLedgerList(ledger));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:finance:list')")
+    @GetMapping("/ledger/summary")
+    public AjaxResult summary() {
+        Map<String, Object> result = new HashMap<>();
+        java.util.List<FinancialLedger> allLedgers = financialLedgerService.list();
+        result.put("totalAmount", financialLedgerService.sumTotalAmount(allLedgers));
+        result.put("details", financialLedgerService.summaryGroupByType());
+        return AjaxResult.success(result);
+    }
+}

+ 59 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/platform/ResourceAuthController.java

@@ -0,0 +1,59 @@
+package com.fs.admin.controller.platform;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.platform.domain.ResourceAuth;
+import com.fs.platform.service.IResourceAuthService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin/resourceAuth")
+public class ResourceAuthController extends BaseController {
+    @Autowired
+    private IResourceAuthService resourceAuthService;
+
+    @PreAuthorize("@ss.hasPermi('admin:resourceAuth:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ResourceAuth auth) {
+        startPage();
+        return getDataTable(resourceAuthService.selectResourceAuthList(auth));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:resourceAuth:add')")
+    @PostMapping
+    @Transactional
+    public AjaxResult add(@RequestBody ResourceAuth auth) {
+        auth.setStatus(1);
+        resourceAuthService.save(auth);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:resourceAuth:add')")
+    @PostMapping("/batch")
+    @Transactional
+    public AjaxResult batchAdd(@RequestBody List<ResourceAuth> authList) {
+        resourceAuthService.batchInsertResourceAuth(authList);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:resourceAuth:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody ResourceAuth auth) {
+        resourceAuthService.updateById(auth);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:resourceAuth:remove')")
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        resourceAuthService.removeByIds(Arrays.asList(ids));
+        return AjaxResult.success();
+    }
+}

+ 32 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/store/DropShippingController.java

@@ -0,0 +1,32 @@
+package com.fs.admin.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.hisStore.service.IDropShippingService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/admin/dropShipping")
+public class DropShippingController extends BaseController {
+    @Autowired
+    private IDropShippingService dropShippingService;
+
+    @PreAuthorize("@ss.hasPermi('admin:dropShipping:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String orderCode,
+                              @RequestParam(required = false) Integer shippingStatus,
+                              @RequestParam(required = false) Long supplierId) {
+        startPage();
+        return getDataTable(dropShippingService.selectDropShippingList(orderCode));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:dropShipping:edit')")
+    @PutMapping("/{orderId}/status")
+    public AjaxResult updateStatus(@PathVariable Long orderId, @RequestParam Integer status) {
+        dropShippingService.updateDropShippingStatus(orderId, status);
+        return AjaxResult.success();
+    }
+}

+ 51 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/store/SupplierAdminController.java

@@ -0,0 +1,51 @@
+package com.fs.admin.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.oms.domain.OmsSupplier;
+import com.fs.oms.service.IOmsSupplierService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/admin/supplier")
+public class SupplierAdminController extends BaseController {
+    @Autowired
+    private IOmsSupplierService omsSupplierService;
+
+    @PreAuthorize("@ss.hasPermi('admin:supplier:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(OmsSupplier supplier) {
+        startPage();
+        List<?> list = omsSupplierService.selectOmsSupplierList(supplier);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:supplier:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(omsSupplierService.selectOmsSupplierById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:supplier:add')")
+    @PostMapping
+    public AjaxResult add(@RequestBody OmsSupplier supplier) {
+        return toAjax(omsSupplierService.insertOmsSupplier(supplier));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:supplier:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody OmsSupplier supplier) {
+        return toAjax(omsSupplierService.updateOmsSupplier(supplier));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:supplier:remove')")
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(omsSupplierService.deleteOmsSupplierByIds(ids));
+    }
+}

+ 48 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/tenant/TenantPackageController.java

@@ -0,0 +1,48 @@
+package com.fs.admin.controller.tenant;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.tenant.domain.TenantPackage;
+import com.fs.tenant.service.ITenantPackageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/admin/tenantPackage")
+public class TenantPackageController extends BaseController {
+    @Autowired
+    private ITenantPackageService tenantPackageService;
+
+    @PreAuthorize("@ss.hasPermi('admin:tenantPackage:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(TenantPackage pkg) {
+        startPage();
+        return getDataTable(tenantPackageService.selectTenantPackageList(pkg));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:tenantPackage:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(tenantPackageService.getById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:tenantPackage:add')")
+    @PostMapping
+    public AjaxResult add(@RequestBody TenantPackage pkg) {
+        return toAjax(tenantPackageService.save(pkg));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:tenantPackage:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody TenantPackage pkg) {
+        return toAjax(tenantPackageService.updateById(pkg));
+    }
+
+    @PreAuthorize("@ss.hasPermi('admin:tenantPackage:remove')")
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(tenantPackageService.removeByIds(java.util.Arrays.asList(ids)));
+    }
+}

+ 9 - 0
java/fs-admin/src/main/java/com/fs/admin/vo/AdminDashboardVO.java

@@ -24,4 +24,13 @@ public class AdminDashboardVO implements Serializable {
 
     /** 总充值金额(ADMIN_RECHARGE 累计) */
     private BigDecimal totalRecharge;
+
+    /** 今日外呼总次数 */
+    private Long todayCallCount;
+
+    /** 外呼总次数(历史累计) */
+    private Long totalCallCount;
+
+    /** 今日新增租户数 */
+    private Long todayNewTenants;
 }

+ 30 - 0
java/fs-agent/src/main/java/com/fs/proxy/controller/ProxyController.java

@@ -27,7 +27,9 @@ import com.fs.proxy.service.ProxyService;
 import com.fs.proxy.service.ProxyServicePriceService;
 import com.fs.proxy.service.ProxyWithdrawService;
 import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.domain.TenantPackage;
 import com.fs.tenant.mapper.TenantInfoMapper;
+import com.fs.tenant.service.ITenantPackageService;
 import com.fs.tenant.service.TenantInfoService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -61,6 +63,9 @@ public class ProxyController extends BaseController
     @Autowired
     private TenantDataSourceContextHelper tenantContextHelper;
 
+    @Autowired
+    private ITenantPackageService tenantPackageService;
+
     @PreAuthorize("@ss.hasPermi('proxy:dashboard:view')")
     @GetMapping("/dashboard")
     public AjaxResult getDashboard() {
@@ -322,6 +327,31 @@ public class ProxyController extends BaseController
                 () -> tenantInfoService.menuAssignReplace(finalSelected, flag, null, assignCompanyMenu));
     }
 
+    // ==================== 租户套餐分配 ====================
+
+    @PreAuthorize("@ss.hasPermi('proxy:tenant:package')")
+    @GetMapping("/tenants/packages")
+    public AjaxResult getPackageList() {
+        TenantPackage query = new TenantPackage();
+        query.setStatus(0);
+        return AjaxResult.success(tenantPackageService.selectTenantPackageList(query));
+    }
+
+    @PreAuthorize("@ss.hasPermi('proxy:tenant:package')")
+    @Log(title = "分配租户套餐", businessType = BusinessType.UPDATE)
+    @PutMapping("/tenants/{id}/package")
+    public AjaxResult assignPackage(@PathVariable Long id, @RequestParam Long packageId) {
+        Long proxyId = getCurrentProxyId();
+        TenantInfo exist = tenantInfoService.selectTenantInfoById(id.toString());
+        if (exist == null || !proxyId.equals(exist.getProxyId())) {
+            return AjaxResult.error("无权操作该租户");
+        }
+        TenantInfo update = new TenantInfo();
+        update.setId(id);
+        update.setPackageId(packageId);
+        return toAjax(tenantInfoService.updateTenantInfo(update));
+    }
+
     private List<SysMenu> loadMasterSysMenus(List<Long> selected) {
         if (selected == null || selected.isEmpty()) {
             return new ArrayList<>();

+ 52 - 0
java/fs-agent/src/main/java/com/fs/proxy/controller/ProxyPurchaseSplitController.java

@@ -0,0 +1,52 @@
+package com.fs.proxy.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.proxy.domain.ProxyPurchaseSplit;
+import com.fs.proxy.service.IProxyPurchaseSplitService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 代理进货分成查询
+ */
+@RestController
+@RequestMapping("/proxy/purchaseSplit")
+public class ProxyPurchaseSplitController extends BaseController {
+
+    @Autowired
+    private IProxyPurchaseSplitService proxyPurchaseSplitService;
+
+    /**
+     * 进货分成列表
+     */
+    @PreAuthorize("@ss.hasPermi('proxy:purchaseSplit:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ProxyPurchaseSplit split) {
+        split.setProxyId(SecurityUtils.getLoginUser().getProxyId());
+        startPage();
+        return getDataTable(proxyPurchaseSplitService.selectPurchaseSplitList(split));
+    }
+
+    /**
+     * 进货分成汇总统计
+     */
+    @PreAuthorize("@ss.hasPermi('proxy:purchaseSplit:list')")
+    @GetMapping("/summary")
+    public AjaxResult summary(@RequestParam(required = false) String month) {
+        Long proxyId = SecurityUtils.getLoginUser().getProxyId();
+        return AjaxResult.success(proxyPurchaseSplitService.purchaseSplitSummary(proxyId, month));
+    }
+
+    /**
+     * 进货分成详情
+     */
+    @PreAuthorize("@ss.hasPermi('proxy:purchaseSplit:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(proxyPurchaseSplitService.getById(id));
+    }
+}

+ 1 - 1
java/fs-saas-admin/pom.xml

@@ -229,7 +229,7 @@
                             <artifactId>annotations-api</artifactId>
                         </exclude>
                     </excludes>
-                    <classifier>exec</classifier> <!-- repackage后的fat jar带exec后缀,原始jar保持普通格式可被其他项目依赖 -->
+
                 </configuration>
                 <executions>
                     <execution>

+ 5 - 0
java/fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceLogsMapper.java

@@ -1,5 +1,6 @@
 package com.fs.company.mapper;
 
+import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -295,4 +296,8 @@ public interface CompanyVoiceLogsMapper
 
     /** 指定公司人工外呼接通次数 */
     Long countManualConnectedByCompany(@Param("companyId") Long companyId);
+
+    /** 全平台今日人工外呼次数(时间范围) */
+    @Select("SELECT COUNT(1) FROM company_voice_logs WHERE start_time >= #{startTime} AND start_time < #{endTime}")
+    Long countManualCallsToday(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
 }

+ 6 - 0
java/fs-service/src/main/java/com/fs/device/service/IDeviceManufacturerService.java

@@ -1,5 +1,6 @@
 package com.fs.device.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.device.domain.DeviceManufacturer;
 
@@ -10,6 +11,11 @@ import java.util.List;
  */
 public interface IDeviceManufacturerService extends IService<DeviceManufacturer> {
 
+    /**
+     * 分页查询厂商列表(管理端)
+     */
+    List<DeviceManufacturer> selectManufacturerList(DeviceManufacturer manufacturer);
+
     /**
      * 查询合作中的厂商列表
      */

+ 13 - 0
java/fs-service/src/main/java/com/fs/device/service/impl/DeviceManufacturerServiceImpl.java

@@ -17,6 +17,19 @@ import java.util.List;
 public class DeviceManufacturerServiceImpl extends ServiceImpl<DeviceManufacturerMapper, DeviceManufacturer>
         implements IDeviceManufacturerService {
 
+    @Override
+    public List<DeviceManufacturer> selectManufacturerList(DeviceManufacturer manufacturer) {
+        LambdaQueryWrapper<DeviceManufacturer> qw = new LambdaQueryWrapper<>();
+        if (manufacturer.getName() != null) {
+            qw.like(DeviceManufacturer::getName, manufacturer.getName());
+        }
+        if (manufacturer.getStatus() != null) {
+            qw.eq(DeviceManufacturer::getStatus, manufacturer.getStatus());
+        }
+        qw.orderByDesc(DeviceManufacturer::getCreateTime);
+        return this.list(qw);
+    }
+
     @Override
     public List<DeviceManufacturer> listActive() {
         LambdaQueryWrapper<DeviceManufacturer> wrapper = new LambdaQueryWrapper<>();

+ 26 - 0
java/fs-service/src/main/java/com/fs/finance/domain/FinancialLedger.java

@@ -0,0 +1,26 @@
+package com.fs.finance.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("financial_ledger")
+public class FinancialLedger {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String orderType;     // DISTRIBUTION/STOCK/SERVICE
+    private Long orderId;
+    private String orderCode;
+    private String fromAccount;   // platform/tenant/agent
+    private Long fromId;
+    private String toAccount;
+    private Long toId;
+    private BigDecimal amount;
+    private BigDecimal fee;       // 手续费
+    private Integer status;       // 0=待入账, 1=已入账
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+    private String remark;
+}

+ 12 - 0
java/fs-service/src/main/java/com/fs/finance/mapper/FinancialLedgerMapper.java

@@ -0,0 +1,12 @@
+package com.fs.finance.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.finance.domain.FinancialLedger;
+import org.apache.ibatis.annotations.Select;
+import java.math.BigDecimal;
+import java.util.Map;
+
+public interface FinancialLedgerMapper extends BaseMapper<FinancialLedger> {
+    @Select("SELECT SUM(amount) as totalAmount, order_type as orderType FROM financial_ledger GROUP BY order_type")
+    java.util.List<Map<String, Object>> summaryGroupByType();
+}

+ 17 - 0
java/fs-service/src/main/java/com/fs/finance/service/IFinancialLedgerService.java

@@ -0,0 +1,17 @@
+package com.fs.finance.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.finance.domain.FinancialLedger;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+public interface IFinancialLedgerService extends IService<FinancialLedger> {
+
+    List<FinancialLedger> selectLedgerList(FinancialLedger ledger);
+
+    List<Map<String, Object>> summaryGroupByType();
+
+    BigDecimal sumTotalAmount(List<FinancialLedger> list);
+}

+ 45 - 0
java/fs-service/src/main/java/com/fs/finance/service/impl/FinancialLedgerServiceImpl.java

@@ -0,0 +1,45 @@
+package com.fs.finance.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.finance.domain.FinancialLedger;
+import com.fs.finance.mapper.FinancialLedgerMapper;
+import com.fs.finance.service.IFinancialLedgerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class FinancialLedgerServiceImpl extends ServiceImpl<FinancialLedgerMapper, FinancialLedger> implements IFinancialLedgerService {
+
+    @Autowired
+    private FinancialLedgerMapper financialLedgerMapper;
+
+    @Override
+    public List<FinancialLedger> selectLedgerList(FinancialLedger ledger) {
+        LambdaQueryWrapper<FinancialLedger> qw = new LambdaQueryWrapper<>();
+        if (ledger.getOrderType() != null) {
+            qw.eq(FinancialLedger::getOrderType, ledger.getOrderType());
+        }
+        if (ledger.getStatus() != null) {
+            qw.eq(FinancialLedger::getStatus, ledger.getStatus());
+        }
+        qw.orderByDesc(FinancialLedger::getCreateTime);
+        return this.list(qw);
+    }
+
+    @Override
+    public List<Map<String, Object>> summaryGroupByType() {
+        return financialLedgerMapper.summaryGroupByType();
+    }
+
+    @Override
+    public BigDecimal sumTotalAmount(List<FinancialLedger> list) {
+        return list.stream()
+                .map(FinancialLedger::getAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+}

+ 3 - 0
java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java

@@ -5,6 +5,7 @@ import java.util.List;
 import com.fs.hisStore.config.MedicalMallConfig;
 import com.fs.hisStore.domain.FsStoreCartScrm;
 import com.fs.hisStore.param.FsStoreCartCountParam;
+import com.fs.hisStore.param.FsStoreMultiCartCountParam;
 import com.fs.hisStore.vo.FsStoreCartQueryVO;
 import com.fs.hisStore.vo.FsStoreCartStoreVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
@@ -116,6 +117,8 @@ public interface FsStoreCartScrmMapper
             "</script>"})
     Integer selectFsStoreCartCount(@Param("maps") FsStoreCartCountParam param);
 
+    Integer selectMultiStoreCartCount(@Param("param") FsStoreMultiCartCountParam param);
+
     Integer selectCartProductTypeCountByUserId(long userId);
 
     @Select({"<script>" +

+ 12 - 0
java/fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -1480,4 +1480,16 @@ public interface FsStoreOrderScrmMapper
                            @Param("refundStatus") Integer refundStatus,
                            @Param("refundPrice") java.math.BigDecimal refundPrice);
 
+    // ==================== 一键代发管理 ====================
+
+    @Select({"<script> " +
+            "SELECT * FROM fs_store_order_scrm WHERE is_channel = 2 " +
+            "<if test='orderCode != null and orderCode != \"\"'> AND order_code LIKE CONCAT('%', #{orderCode}, '%')</if> " +
+            "ORDER BY create_time DESC" +
+            "</script>"})
+    List<FsStoreOrderScrm> selectDropShippingList(@Param("orderCode") String orderCode);
+
+    @Update("UPDATE fs_store_order_scrm SET status = #{status} WHERE id = #{id}")
+    int updateDropShippingStatus(@Param("id") Long id, @Param("status") Integer status);
+
 }

+ 21 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreConfirmMultiStoreParam.java

@@ -0,0 +1,21 @@
+package com.fs.hisStore.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 多店铺确认订单参数(购物车自动按 store_id 分组)
+ *
+ * @author system
+ * @date 2026-08-10
+ */
+@Data
+public class FsStoreConfirmMultiStoreParam implements Serializable {
+
+    @NotBlank(message = "请提交购买的商品")
+    @ApiModelProperty(value = "购物车ID,多个逗号分隔")
+    private String cartIds;
+}

+ 19 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreMultiCartCountParam.java

@@ -0,0 +1,19 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class FsStoreMultiCartCountParam implements Serializable {
+
+    /** 用户ID(后端从登录态取,前端可不传) */
+    Long userId;
+
+    /** 商品ID(可选) */
+    Long productId;
+
+    /** 店铺ID列表 */
+    Long storeId;
+}

+ 9 - 19
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreMultiStoreOrderCreateParam.java

@@ -3,16 +3,13 @@ package com.fs.hisStore.param;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
-import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 import javax.validation.constraints.Size;
 import java.io.Serializable;
-import java.math.BigDecimal;
 import java.util.List;
 
 /**
- * 合并多店铺下单参数
+ * 合并多店铺下单参数(购物车自动按 store_id 分组)
  *
  * @author fs
  */
@@ -41,20 +38,13 @@ public class FsStoreMultiStoreOrderCreateParam implements Serializable {
     @ApiModelProperty(value = "媒体来源")
     private Integer orderMedium;
 
-    /** 各店铺购物车分组 */
-    @Valid
     @NotEmpty(message = "请选择下单商品")
-    @ApiModelProperty(value = "各店铺购物车分组")
-    private List<StoreCartGroup> storeCarts;
-
-    @Data
-    public static class StoreCartGroup implements Serializable {
-        @NotNull(message = "店铺ID不能为空")
-        @ApiModelProperty(value = "店铺ID")
-        private Long storeId;
-
-        @NotEmpty(message = "购物车ID列表不能为空")
-        @ApiModelProperty(value = "购物车ID列表")
-        private List<Long> cartIds;
-    }
+    @ApiModelProperty(value = "多店铺订单orderKey列表,用于从Redis获取缓存的购物车数据")
+    private List<String> orderKeys;
+
+    @ApiModelProperty(value = "销售公司用户ID")
+    private Long companyUserId;
+
+    @ApiModelProperty(value = "推荐人用户ID")
+    private Long tuiUserId;
 }

+ 31 - 0
java/fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderMultiStoreComputedParam.java

@@ -0,0 +1,31 @@
+package com.fs.hisStore.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 多店铺计算订单金额参数(orderKey 列表,购物车自带 storeId 自动分组)
+ *
+ * @author system
+ * @date 2026-08-10
+ */
+@Data
+public class FsStoreOrderMultiStoreComputedParam implements Serializable {
+
+    @ApiModelProperty(value = "地址ID")
+    private Long addressId;
+
+    @ApiModelProperty(value = "使用积分 1使用")
+    private Integer useIntegral;
+
+    @ApiModelProperty(value = "优惠券用户ID")
+    private Long couponUserId;
+
+    @NotEmpty(message = "订单信息不能为空")
+    @ApiModelProperty(value = "确认订单返回的orderKey列表")
+    private List<String> orderKeys;
+}

+ 12 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/IDropShippingService.java

@@ -0,0 +1,12 @@
+package com.fs.hisStore.service;
+
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+
+import java.util.List;
+
+public interface IDropShippingService {
+
+    List<FsStoreOrderScrm> selectDropShippingList(String orderCode);
+
+    boolean updateDropShippingStatus(Long orderId, Integer status);
+}

+ 11 - 12
java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCartScrmService.java

@@ -4,24 +4,21 @@ import java.util.List;
 
 import com.fs.common.core.domain.R;
 import com.fs.hisStore.domain.FsStoreCartScrm;
-import com.fs.hisStore.param.FsStoreCartCountParam;
-import com.fs.hisStore.param.FsStoreCartDelParam;
-import com.fs.hisStore.param.FsStoreCartNumParam;
-import com.fs.hisStore.param.FsStoreCartParam;
+import com.fs.hisStore.param.*;
 import com.fs.hisStore.vo.FsStoreCartStoreVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
 
 /**
  * 购物车Service接口
- * 
+ *
  * @author fs
  * @date 2022-03-21
  */
-public interface IFsStoreCartScrmService 
+public interface IFsStoreCartScrmService
 {
     /**
      * 查询购物车
-     * 
+     *
      * @param id 购物车ID
      * @return 购物车
      */
@@ -29,7 +26,7 @@ public interface IFsStoreCartScrmService
 
     /**
      * 查询购物车列表
-     * 
+     *
      * @param fsStoreCart 购物车
      * @return 购物车集合
      */
@@ -37,7 +34,7 @@ public interface IFsStoreCartScrmService
 
     /**
      * 新增购物车
-     * 
+     *
      * @param fsStoreCart 购物车
      * @return 结果
      */
@@ -45,7 +42,7 @@ public interface IFsStoreCartScrmService
 
     /**
      * 修改购物车
-     * 
+     *
      * @param fsStoreCart 购物车
      * @return 结果
      */
@@ -53,7 +50,7 @@ public interface IFsStoreCartScrmService
 
     /**
      * 批量删除购物车
-     * 
+     *
      * @param ids 需要删除的购物车ID
      * @return 结果
      */
@@ -61,7 +58,7 @@ public interface IFsStoreCartScrmService
 
     /**
      * 删除购物车信息
-     * 
+     *
      * @param id 购物车ID
      * @return 结果
      */
@@ -101,5 +98,7 @@ public interface IFsStoreCartScrmService
 
     Integer selectFsStoreCartCount(FsStoreCartCountParam param);
 
+    Integer selectMultiStoreCartCount(FsStoreMultiCartCountParam param);
+
     Integer selectCartProductTypeCountByUserId(long userId);
 }

+ 16 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -357,6 +357,22 @@ public interface IFsStoreOrderScrmService
      */
     R createMultiStoreOrder(long userId, FsStoreMultiStoreOrderCreateParam param);
 
+    /**
+     * 多店铺确认订单(生成orderKey缓存购物车)
+     * @param userId 用户ID
+     * @param param  多店铺确认参数
+     * @return 各店铺确认结果
+     */
+    R confirmMultiStore(long userId, FsStoreConfirmMultiStoreParam param);
+
+    /**
+     * 多店铺计算订单金额
+     * @param userId 用户ID
+     * @param param  多店铺计算参数
+     * @return 各店铺费用汇总
+     */
+    R computedMultiStore(long userId, FsStoreOrderMultiStoreComputedParam param);
+
     /**
      * 整单退款(用户端)
      * @param orderId 订单ID

+ 26 - 0
java/fs-service/src/main/java/com/fs/hisStore/service/impl/DropShippingServiceImpl.java

@@ -0,0 +1,26 @@
+package com.fs.hisStore.service.impl;
+
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.service.IDropShippingService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class DropShippingServiceImpl implements IDropShippingService {
+
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
+
+    @Override
+    public List<FsStoreOrderScrm> selectDropShippingList(String orderCode) {
+        return fsStoreOrderScrmMapper.selectDropShippingList(orderCode);
+    }
+
+    @Override
+    public boolean updateDropShippingStatus(Long orderId, Integer status) {
+        return fsStoreOrderScrmMapper.updateDropShippingStatus(orderId, status) > 0;
+    }
+}

+ 6 - 4
java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java

@@ -20,10 +20,7 @@ import com.fs.hisStore.mapper.FsStoreCartScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductGroupScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
-import com.fs.hisStore.param.FsStoreCartCountParam;
-import com.fs.hisStore.param.FsStoreCartDelParam;
-import com.fs.hisStore.param.FsStoreCartNumParam;
-import com.fs.hisStore.param.FsStoreCartParam;
+import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.IFsStoreCartScrmService;
 import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
@@ -575,6 +572,11 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
         return  fsStoreCartMapper.selectFsStoreCartCount(param);
     }
 
+    @Override
+    public Integer selectMultiStoreCartCount(FsStoreMultiCartCountParam param) {
+        return fsStoreCartMapper.selectMultiStoreCartCount(param);
+    }
+
     @Override
     public Integer selectCartProductTypeCountByUserId(long userId) {
         return fsStoreCartMapper.selectCartProductTypeCountByUserId(userId);

+ 409 - 69
java/fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -3287,6 +3287,22 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return priceGroupDTO;
     }
 
+    /**
+     * 多店铺版:计算单个店铺的金额和运费(含偏远地区校验)
+     * 与 getOrderPriceGroup 的区别:调用方为多店铺流程,内部已处理 postage=-1 异常
+     *
+     * @param carts       该店铺的购物车列表
+     * @param userAddress 收货地址(可为null)
+     * @return FsStoreOrderPriceDTO
+     */
+    private FsStoreOrderPriceDTO computeStorePriceGroup(List<FsStoreCartQueryVO> carts, FsUserAddressScrm userAddress) {
+        FsStoreOrderPriceDTO priceGroup = this.getOrderPriceGroup(carts, userAddress);
+        if (priceGroup.getStorePostage().compareTo(BigDecimal.valueOf(-1)) == 0) {
+            throw new ServiceException("偏远地区暂不可购买");
+        }
+        return priceGroup;
+    }
+
     /**
      * 根据运费模板算法返回邮费
      *
@@ -5681,6 +5697,206 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return prefix + String.format(keyFormat, args);
     }
 
+    /**
+     * 构建带租户前缀的缓存 key(与 buildTenantLockKey 一致)
+     * 多租户: 1:orderKey:xxx   单租户: orderKey:xxx
+     */
+    private String buildTenantCacheKey(String suffix) {
+        Long tenantId = DynamicDataSourceContextHolder.getCurrentTenantId();
+        String prefix = tenantId != null ? tenantId + ":" : "";
+        return prefix + suffix;
+    }
+
+    // ==================== 多店铺确认 & 计算 ====================
+
+    @Override
+    public R confirmMultiStore(long userId, FsStoreConfirmMultiStoreParam param) {
+        // 1. 获取默认地址
+        FsUserAddressScrm address = userAddressMapper.selectFsUserAddressByDefaultAddress(userId);
+
+        // 2. 解析 cartIds 字符串 -> List<Long>
+        List<Long> allCartIds = new ArrayList<>();
+        for (String idStr : param.getCartIds().split(",")) {
+            String trimmed = idStr.trim();
+            if (!trimmed.isEmpty()) {
+                allCartIds.add(Long.parseLong(trimmed));
+            }
+        }
+        if (allCartIds.isEmpty()) {
+            return R.error("购物车ID列表不能为空");
+        }
+
+        // 3. 查询购物车数据(含店铺信息,自动按 store_id 分组)
+        List<FsStoreCartStoreVO> allCarts = fsStoreOrderMapper.selectCartByCartIds(allCartIds);
+        if (allCarts == null || allCarts.isEmpty()) {
+            return R.error("购物车数据不存在或已过期");
+        }
+
+        // 4. 按店铺分组
+        Map<Long, List<FsStoreCartStoreVO>> storeCartMap = new LinkedHashMap<>();
+        for (FsStoreCartStoreVO cart : allCarts) {
+            storeCartMap.computeIfAbsent(cart.getStoreId(), k -> new ArrayList<>()).add(cart);
+        }
+
+        // 5. 逐店铺处理:生成 orderKey、缓存到 Redis
+        FsStoreMultiStoreConfirmVO result = new FsStoreMultiStoreConfirmVO();
+        result.setAddress(address);
+        List<FsStoreMultiStoreConfirmVO.StoreConfirmEntry> entries = new ArrayList<>();
+
+        for (Map.Entry<Long, List<FsStoreCartStoreVO>> entry : storeCartMap.entrySet()) {
+            Long storeId = entry.getKey();
+            List<FsStoreCartStoreVO> storeCarts = entry.getValue();
+
+            String uuid = IdUtil.randomUUID();
+            String cartIdsStr = String.join(",",
+                    storeCarts.stream().map(c -> String.valueOf(c.getId())).collect(Collectors.toList()));
+
+            // 缓存 cartIds 和 carts(带租户前缀,与 buildTenantLockKey 一致,TTL=300秒)
+            redisCache.setCacheObject(buildTenantCacheKey("orderKey:" + uuid), cartIdsStr, 300, TimeUnit.SECONDS);
+            redisCache.setCacheObject(buildTenantCacheKey("orderCarts:" + uuid), storeCarts, 300, TimeUnit.SECONDS);
+
+            FsStoreMultiStoreConfirmVO.StoreConfirmEntry confirmEntry = new FsStoreMultiStoreConfirmVO.StoreConfirmEntry();
+            confirmEntry.setStoreId(storeId);
+            confirmEntry.setStoreName(storeCarts.get(0).getStoreName());
+            confirmEntry.setOrderKey(uuid);
+            confirmEntry.setCarts(storeCarts);
+            entries.add(confirmEntry);
+
+            log.info("多店铺确认: storeId={}, orderKey={}, cartCount={}", storeId, uuid, storeCarts.size());
+        }
+
+        result.setStoreConfirms(entries);
+        return R.ok().put("data", result);
+    }
+
+    @Override
+    public R computedMultiStore(long userId, FsStoreOrderMultiStoreComputedParam param) {
+        log.info("computedMultiStore userId={}, orderKeyCount={}", userId, param.getOrderKeys().size());
+
+        // 汇总
+        BigDecimal totalPrice = BigDecimal.ZERO;
+        BigDecimal totalPostage = BigDecimal.ZERO;
+        BigDecimal totalServiceFee = BigDecimal.ZERO;
+        Map<Long, BigDecimal> storeTotalPriceMap = new LinkedHashMap<>();
+
+        List<FsStoreMultiStoreComputedVO.StoreComputedEntry> storeEntries = new ArrayList<>();
+
+        for (String orderKey : param.getOrderKeys()) {
+
+            // 从 Redis 取出该店铺的购物车(带租户前缀)
+            String cartIds = redisCache.getCacheObject(buildTenantCacheKey("orderKey:" + orderKey));
+            if (ObjectUtil.isNull(cartIds)) {
+                throw new CustomException("店铺订单已过期,请重新确认", 501);
+            }
+            List<FsStoreCartStoreVO> carts = redisCache.getCacheObject(buildTenantCacheKey("orderCarts:" + orderKey));
+            if (carts == null || carts.isEmpty()) {
+                throw new CustomException("店铺购物车数据已过期", 501);
+            }
+
+            // 从购物车记录中获取 storeId(无需前端传)
+            Long storeId = carts.get(0).getStoreId();
+            String storeName = carts.get(0).getStoreName();
+
+            // 多店铺版:计算该店铺金额和运费(内部已调用 getOrderPriceGroup + 偏远地区校验)
+            FsUserAddressScrm userAddress = null;
+            if (param.getAddressId() != null) {
+                userAddress = userAddressMapper.selectFsUserAddressById(param.getAddressId());
+            }
+            @SuppressWarnings("unchecked")
+            List<FsStoreCartQueryVO> storeCarts = (List<FsStoreCartQueryVO>) (List<?>) carts;
+            FsStoreOrderPriceDTO priceGroup = this.computeStorePriceGroup(storeCarts, userAddress);
+            BigDecimal payPostage = priceGroup.getStorePostage();
+
+            // 累加汇总
+            totalPrice = totalPrice.add(priceGroup.getTotalPrice());
+            totalPostage = totalPostage.add(payPostage);
+            storeTotalPriceMap.put(storeId, priceGroup.getTotalPrice());
+
+            FsStoreMultiStoreComputedVO.StoreComputedEntry entry = new FsStoreMultiStoreComputedVO.StoreComputedEntry();
+            entry.setStoreId(storeId);
+            entry.setStoreName(
+                     !carts.get(0).getStoreName().isBlank()?carts.get(0).getStoreName():""
+
+            );
+            entry.setTotalPrice(priceGroup.getTotalPrice());
+            entry.setPayPostage(payPostage);
+            entry.setPayPrice(NumberUtil.add(priceGroup.getTotalPrice(), payPostage));
+            storeEntries.add(entry);
+        }
+
+        // 加服务费(统一加一次)
+        String configJson = configService.selectConfigByKey("store.config");
+        StoreConfig config = JSONUtil.toBean(configJson, StoreConfig.class);
+        if (config.getServiceFee() != null) {
+            totalServiceFee = config.getServiceFee();
+        }
+        BigDecimal payPrice = NumberUtil.add(NumberUtil.add(totalPrice, totalPostage), totalServiceFee);
+
+        // 积分抵扣(统一处理)
+        BigDecimal deductionPrice = BigDecimal.ZERO;
+        double usedIntegral = 0;
+        FsUserScrm user = userService.selectFsUserById(userId);
+        if (param.getUseIntegral() != null && param.getUseIntegral() == 1
+                && user != null && user.getIntegral() != null && user.getIntegral().intValue() > 0) {
+            Double integralMax = 100.0;
+            BigDecimal integralFull = new BigDecimal(100);
+            Double integralRatio = 1.0;
+            String integralJson = configService.selectConfigByKey("store.integral");
+            if (StringUtils.isNotEmpty(integralJson)) {
+                StoreIntegralConfig integralConfig = JSONUtil.toBean(integralJson, StoreIntegralConfig.class);
+                integralMax = integralConfig.getIntegralMax();
+                integralFull = integralConfig.getIntegralFull();
+                integralRatio = integralConfig.getIntegralRatio();
+            }
+            if (totalPrice.compareTo(integralFull) >= 0) {
+                Double userIntegral = user.getIntegral().doubleValue();
+                if (integralMax.intValue() > 0 && Double.compare(userIntegral, integralMax) >= 0) {
+                    userIntegral = integralMax;
+                }
+                deductionPrice = BigDecimal.valueOf(NumberUtil.mul(userIntegral, integralRatio));
+                if (deductionPrice.compareTo(payPrice) < 0) {
+                    payPrice = NumberUtil.sub(payPrice, deductionPrice);
+                    usedIntegral = userIntegral;
+                } else {
+                    deductionPrice = payPrice;
+                    payPrice = BigDecimal.ZERO;
+                    usedIntegral = NumberUtil.round(
+                            NumberUtil.div(deductionPrice, BigDecimal.valueOf(integralRatio)), 2).doubleValue();
+                }
+            }
+        }
+
+        if (payPrice.compareTo(BigDecimal.ZERO) <= 0) {
+            payPrice = BigDecimal.ZERO;
+        }
+
+        // 优惠券(统一处理)
+        BigDecimal couponDeduction = BigDecimal.ZERO;
+        if (param.getCouponUserId() != null) {
+            FsStoreCouponUserScrm couponUser = couponUserService.selectFsStoreCouponUserById(param.getCouponUserId());
+            if (couponUser != null && couponUser.getStatus() == 0) {
+                if (couponUser.getUseMinPrice().compareTo(totalPrice) < 0) {
+                    couponDeduction = couponUser.getCouponPrice();
+                    payPrice = payPrice.subtract(couponDeduction);
+                }
+            }
+        }
+
+        // 组装返回
+        FsStoreMultiStoreComputedVO resultVO = new FsStoreMultiStoreComputedVO();
+        resultVO.setTotalPrice(totalPrice);
+        resultVO.setPayPrice(payPrice);
+        resultVO.setPayPostage(totalPostage);
+        resultVO.setDeductionPrice(NumberUtil.add(deductionPrice, couponDeduction));
+        resultVO.setUsedIntegral(BigDecimal.valueOf(usedIntegral));
+        resultVO.setServiceFee(totalServiceFee);
+        resultVO.setStoreComputeds(storeEntries);
+
+        return R.ok().put("data", resultVO);
+    }
+
+    // ==================== 合并多店铺下单 ====================
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public R createMultiStoreOrder(long userId, FsStoreMultiStoreOrderCreateParam param) {
@@ -5692,24 +5908,28 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 return R.error("您有订单正在处理中,请稍后再试");
             }
 
-            // 1. 收集所有 cartIds 并查询购物车信息(含店铺ID)
-            List<Long> allCartIds = new ArrayList<>();
-            for (FsStoreMultiStoreOrderCreateParam.StoreCartGroup group : param.getStoreCarts()) {
-                allCartIds.addAll(group.getCartIds());
-            }
-            if (allCartIds.isEmpty()) {
-                return R.error("购物车ID列表不能为空");
+            // 1. 从 Redis 获取购物车数据(与 createOrder 一致,通过 orderKey 获取)
+            List<String> orderKeys = param.getOrderKeys();
+            if (orderKeys == null || orderKeys.isEmpty()) {
+                return R.error("订单Key列表不能为空");
             }
 
-            List<FsStoreCartStoreVO> allCarts = fsStoreOrderMapper.selectCartByCartIds(allCartIds);
-            if (allCarts == null || allCarts.isEmpty()) {
-                return R.error("购物车数据不存在或已过期");
-            }
-
-            // 2. 按店铺分组
-            Map<Long, List<FsStoreCartStoreVO>> storeCartMap = new HashMap<>();
-            for (FsStoreCartStoreVO cart : allCarts) {
-                storeCartMap.computeIfAbsent(cart.getStoreId(), k -> new ArrayList<>()).add(cart);
+            Map<Long, List<FsStoreCartStoreVO>> storeCartMap = new LinkedHashMap<>();
+            for (String orderKey : orderKeys) {
+                String tenantOrderKey = buildTenantCacheKey("orderKey:" + orderKey);
+                String cartIdsStr = redisCache.getCacheObject(tenantOrderKey);
+                if (cartIdsStr == null) {
+                    return R.error("订单已过期, 请重新下单");
+                }
+                String tenantOrderCartsKey = buildTenantCacheKey("orderCarts:" + orderKey);
+                @SuppressWarnings("unchecked")
+                List<FsStoreCartStoreVO> storeCarts = redisCache.getCacheObject(tenantOrderCartsKey);
+                if (storeCarts == null || storeCarts.isEmpty()) {
+                    return R.error("订单购物车数据已过期, 请重新下单");
+                }
+                for (FsStoreCartStoreVO cart : storeCarts) {
+                    storeCartMap.computeIfAbsent(cart.getStoreId(), k -> new ArrayList<>()).add(cart);
+                }
             }
 
             // 3. 获取用户地址
@@ -5718,14 +5938,95 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 address = userAddressMapper.selectFsUserAddressById(param.getAddressId());
             }
 
-            // 4. 获取用户信息
+            // 获取用户信息
             FsUserScrm user = userService.selectFsUserById(userId);
 
-            // 5. 按店铺逐个创建订单
+            // 与 createOrder 一致:配置&销售绑定逻辑
+            String json = configService.selectConfigByKey("store.config");
+            StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+            FsUserScrm fsuser = userService.selectFsUserById(userId);
+            if (ObjectUtil.isEmpty(config.getOrderAttribution())
+                    || !config.getOrderAttribution().equals(1)) {
+                if (param.getCompanyUserId() != null) {
+                    if (ObjectUtil.isNotEmpty(fsuser.getCompanyUserId())
+                            && !fsuser.getCompanyUserId().equals(param.getCompanyUserId())) {
+                        CompanyUser companyUser = companyUserService.selectCompanyUserById(fsuser.getCompanyUserId());
+                        return R.error(String.format("请联系%s销售进行购买商品!", companyUser.getNickName()));
+                    } else {
+                        fsuser.setCompanyUserId(param.getCompanyUserId());
+                        userService.updateFsUser(fsuser);
+                    }
+                }
+            }
+
+            CompanyUserUser map = new CompanyUserUser();
+            map.setCompanyUserId(param.getCompanyUserId());
+            map.setUserId(userId);
+            List<CompanyUserUser> list = companyUserUserMapper.selectCompanyUserUserList(map);
+            if (list == null || list.size() == 0) {
+                CompanyUser companyUser = companyUserService.selectCompanyUserById(param.getCompanyUserId());
+                if (companyUser != null && companyUser.getStatus().equals("0")) {
+                    map.setCompanyId(companyUser.getCompanyId());
+                    companyUserUserMapper.insertCompanyUserUser(map);
+                }
+            }
+
+            // 汇总所有店铺的购物车
+            List<FsStoreCartStoreVO> allCarts = new ArrayList<>();
+            StringBuilder allCartIdsBuilder = new StringBuilder();
+            for (Map.Entry<Long, List<FsStoreCartStoreVO>> entry : storeCartMap.entrySet()) {
+                for (FsStoreCartStoreVO cart : entry.getValue()) {
+                    allCarts.add(cart);
+                    if (allCartIdsBuilder.length() > 0) allCartIdsBuilder.append(",");
+                    allCartIdsBuilder.append(cart.getId());
+                }
+            }
+            String allCartIds = allCartIdsBuilder.toString();
+            @SuppressWarnings("unchecked")
+            List<FsStoreCartQueryVO> allCartQueryVOs = (List<FsStoreCartQueryVO>) (List<?>) allCarts;
+
+            // 优惠券处理(与 createOrder 一致)
+            Long couponId = null;
+            BigDecimal couponPrice = BigDecimal.ZERO;
+            if (param.getCouponUserId() != null) {
+                FsStoreCouponUserScrm couponUser = couponUserService.selectFsStoreCouponUserById(param.getCouponUserId());
+                if (couponUser != null && couponUser.getStatus() == 0) {
+                    couponId = couponUser.getId();
+                    couponPrice = couponUser.getCouponPrice();
+                    couponUser.setStatus(1);
+                    couponUser.setUseTime(new Date());
+                    couponUserService.updateFsStoreCouponUser(couponUser);
+                }
+            }
+
+            // 推荐人处理(与 createOrder 一致)
+            Long tuiUserId = null;
+            if (user.getSpreadUserId() != null && user.getSpreadUserId() > 0) {
+                tuiUserId = user.getSpreadUserId();
+            } else {
+                if (param.getTuiUserId() != null) {
+                    FsUserScrm tuiUser = userService.selectFsUserById(param.getTuiUserId());
+                    if (tuiUser != null && tuiUser.getIsPromoter() == 1) {
+                        tuiUserId = param.getTuiUserId();
+                    }
+                }
+            }
+
+            // 减库存加销量(与 createOrder 一致,批量处理)
+            this.deStockIncSale(allCartQueryVOs);
+
+            // 按店铺逐个创建订单
             FsStoreMultiStoreOrderResultVO result = new FsStoreMultiStoreOrderResultVO();
             List<FsStoreMultiStoreOrderResultVO.StoreOrderEntry> orderEntries = new ArrayList<>();
             BigDecimal totalAmount = BigDecimal.ZERO;
 
+            // 先计算总邮费和总实付(整单邮费共享)
+            FsStoreOrderPriceDTO totalPriceGroup = getOrderPriceGroup(allCartQueryVOs, address);
+            BigDecimal totalPayPostage = totalPriceGroup.getStorePostage();
+            if (totalPayPostage.compareTo(BigDecimal.valueOf(-1)) == 0) {
+                throw new ServiceException("偏远地区暂不可购买");
+            }
+
             for (Map.Entry<Long, List<FsStoreCartStoreVO>> entry : storeCartMap.entrySet()) {
                 Long storeId = entry.getKey();
                 List<FsStoreCartStoreVO> storeCarts = entry.getValue();
@@ -5734,32 +6035,19 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 // 生成订单号
                 String orderSn = OrderCodeUtils.getOrderSn();
 
-                // 组装成 FsStoreCartQueryVO 以复用计价逻辑
-                List<FsStoreCartQueryVO> cartQueryVOs = new ArrayList<>();
+                @SuppressWarnings("unchecked")
+                List<FsStoreCartQueryVO> cartQueryVOs = (List<FsStoreCartQueryVO>) (List<?>) storeCarts;
                 StringBuilder cartIdSb = new StringBuilder();
-                for (FsStoreCartStoreVO cart : storeCarts) {
-                    FsStoreCartQueryVO vo = new FsStoreCartQueryVO();
-                    vo.setId(cart.getId());
-                    vo.setProductId(cart.getProductId());
-                    vo.setCartNum(cart.getCartNum());
-                    vo.setPrice(cart.getPrice());
-                    vo.setProductName(cart.getProductName());
-                    vo.setProductImage(cart.getProductImage());
-                    cartQueryVOs.add(vo);
+                for (FsStoreCartQueryVO cart : cartQueryVOs) {
                     if (cartIdSb.length() > 0) cartIdSb.append(",");
                     cartIdSb.append(cart.getId());
                 }
 
-                // 计算价
+                // 计算店铺商品总价
                 BigDecimal payPrice = getOrderSumPrice(cartQueryVOs, "truePrice");
-                FsStoreOrderPriceDTO priceGroup = this.getOrderPriceGroup(cartQueryVOs, address);
-                BigDecimal payPostage = priceGroup.getStorePostage();
-                if (payPostage.compareTo(BigDecimal.valueOf(-1)) == 0) {
-                    throw new ServiceException("店铺[" + storeName + "]偏远地区暂不可购买");
-                }
-                payPrice = NumberUtil.add(payPrice, payPostage);
+                payPrice = NumberUtil.add(payPrice, totalPayPostage);
 
-                // 构建订单实体
+                // 构建订单实体(与 createOrder 一致)
                 FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
                 storeOrder.setStoreId(storeId);
                 storeOrder.setStoreHouseCode("CK01");
@@ -5768,9 +6056,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 storeOrder.setCartId(cartIdSb.toString());
                 storeOrder.setTotalNum((long) storeCarts.size());
                 storeOrder.setTotalPrice(payPrice);
-                storeOrder.setTotalPostage(payPostage);
-                storeOrder.setPayPostage(payPostage);
-                storeOrder.setDeductionPrice(priceGroup.getCouponPrice());
+                storeOrder.setTotalPostage(totalPayPostage);
+                storeOrder.setPayPostage(totalPayPostage);
+                storeOrder.setDeductionPrice(totalPriceGroup.getCouponPrice());
                 storeOrder.setPaid(0);
                 storeOrder.setPayType(param.getPayType());
                 storeOrder.setStatus(0);
@@ -5783,6 +6071,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 storeOrder.setIsSysDel(0);
                 storeOrder.setOrderType(param.getOrderType());
                 storeOrder.setOrderMedium(param.getOrderMedium());
+                storeOrder.setCompanyUserId(param.getCompanyUserId());
+                storeOrder.setCreateTime(new Date());
 
                 // 成本价
                 BigDecimal costPrice = this.getOrderSumPrice(cartQueryVOs, "costPrice");
@@ -5796,45 +6086,77 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                             + " " + address.getDistrict() + " " + address.getDetail().trim());
                 }
 
-                // 插入订单
-                fsStoreOrderMapper.insertFsStoreOrder(storeOrder);
+                // 处方药标记
+                long prescribe = cartQueryVOs.stream()
+                        .filter(item -> item.getProductType() != null && item.getProductType() == 2).count();
+                storeOrder.setIsPrescribe(prescribe > 0 ? 1 : 0);
+
+                // 服务费
+                if (config.getServiceFee() != null) {
+                    storeOrder.setServiceFee(config.getServiceFee());
+                }
+
+                storeOrder.setPayPrice(payPrice);
+
+                // 推荐人
+                if (tuiUserId != null) {
+                    storeOrder.setTuiUserId(tuiUserId);
+                }
+
+                Integer flag = fsStoreOrderMapper.insertFsStoreOrder(storeOrder);
+                if (flag == 0) {
+                    throw new ServiceException("店铺[" + storeName + "]订单创建失败");
+                }
                 Long orderId = storeOrder.getId();
 
-                // 插入订单明细和扣减库存
-                for (FsStoreCartQueryVO cartVO : cartQueryVOs) {
+                // 保存OrderItem(与 createOrder 一致,使用 FsStoreCartDTO)
+                List<FsStoreOrderItemScrm> listOrderItem = new ArrayList<>();
+                for (FsStoreCartQueryVO vo : cartQueryVOs) {
+                    // 检查限购
+                    checkAndRecordPurchaseLimit(userId, vo.getProductId(), vo.getCartNum());
+
+                    FsStoreCartDTO fsStoreCartDTO = new FsStoreCartDTO();
+                    fsStoreCartDTO.setProductId(vo.getProductId());
+                    fsStoreCartDTO.setPrice(vo.getPrice());
+                    fsStoreCartDTO.setSku(vo.getProductAttrName());
+                    fsStoreCartDTO.setProductName(vo.getProductName());
+                    fsStoreCartDTO.setNum(vo.getCartNum());
+                    fsStoreCartDTO.setBarCode(vo.getBarCode());
+                    fsStoreCartDTO.setGroupBarCode(vo.getGroupBarCode());
+                    fsStoreCartDTO.setBrokerage(vo.getBrokerage());
+                    fsStoreCartDTO.setBrokerageTwo(vo.getBrokerageTwo());
+                    fsStoreCartDTO.setBrokerageThree(vo.getBrokerageThree());
+                    if (StringUtils.isEmpty(vo.getProductAttrImage())) {
+                        fsStoreCartDTO.setImage(vo.getProductImage());
+                    } else {
+                        fsStoreCartDTO.setImage(vo.getProductAttrImage());
+                    }
+
                     FsStoreOrderItemScrm item = new FsStoreOrderItemScrm();
                     item.setOrderId(orderId);
                     item.setOrderCode(orderSn);
-                    item.setProductId(cartVO.getProductId());
-                    item.setCartId(cartVO.getId());
-                    item.setNum(cartVO.getCartNum());
+                    item.setCartId(vo.getId());
+                    item.setProductId(vo.getProductId());
+                    item.setJsonInfo(JSONUtil.toJsonStr(fsStoreCartDTO));
+                    item.setNum(vo.getCartNum());
+                    item.setIsAfterSales(0);
                     item.setStoreId(storeId);
-
-                    JSONObject itemJson = new JSONObject();
-                    itemJson.put("productName", cartVO.getProductName());
-                    itemJson.put("productImage", cartVO.getProductImage());
-                    itemJson.put("price", cartVO.getPrice());
-                    itemJson.put("num", cartVO.getCartNum());
-                    item.setJsonInfo(itemJson.toJSONString());
-                    fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
-
-                    // 扣减库存
-                    FsStoreProductScrm product = productService.selectFsStoreProductById(cartVO.getProductId());
-                    if (product != null) {
-                        if (product.getStock() >= cartVO.getCartNum()) {
-                            product.setStock(product.getStock() - cartVO.getCartNum());
-                            productService.updateFsStoreProduct(product);
-                        }
+                    if (vo.getProductType() != null && vo.getProductType().equals(2)) {
+                        item.setIsPrescribe(1);
                     }
+                    fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+                    listOrderItem.add(item);
                 }
-
-                // 清理购物车
-                for (FsStoreCartQueryVO cartVO : cartQueryVOs) {
-                    FsStoreCartScrm deleteCart = new FsStoreCartScrm();
-                    deleteCart.setId(cartVO.getId());
-                    cartMapper.deleteFsStoreCartByIds(new Long[]{cartVO.getId()});
+                if (listOrderItem.size() > 0) {
+                    String itemJson = JSONUtil.toJsonStr(listOrderItem);
+                    storeOrder.setItemJson(itemJson);
+                    fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
                 }
 
+                // 添加订单日志
+                orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
+                        OrderLogEnum.CREATE_ORDER.getDesc());
+
                 // 组装返回结果
                 FsStoreMultiStoreOrderResultVO.StoreOrderEntry storeOrderEntry = new FsStoreMultiStoreOrderResultVO.StoreOrderEntry();
                 storeOrderEntry.setStoreId(storeId);
@@ -5845,6 +6167,24 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 orderEntries.add(storeOrderEntry);
 
                 totalAmount = totalAmount.add(payPrice);
+
+                // 超时取消(与 createOrder 一致)
+                if (config.getUnPayTime() != null && config.getUnPayTime() > 0) {
+                    String redisKey = String.valueOf(StrUtil.format("{}{}",
+                            StoreConstants.REDIS_ORDER_OUTTIME_UNPAY, storeOrder.getId()));
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                }
+            }
+
+            // 购物车状态修改(不删除,与 createOrder 一致:updateIsPay)
+            if (StringUtils.isNotEmpty(allCartIds)) {
+                cartMapper.updateIsPay(allCartIds);
+            }
+
+            // 删除 Redis 缓存(与 createOrder 一致)
+            for (String orderKey : orderKeys) {
+                redisCache.deleteObject("orderKey:" + orderKey);
+                redisCache.deleteObject("orderCarts:" + orderKey);
             }
 
             result.setOrderCount(orderEntries.size());

+ 1 - 1
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartGroupVO.java

@@ -21,6 +21,6 @@ public class FsStoreCartGroupVO implements Serializable {
     private String storeName;
 
     /** 该店铺下的购物车列表 */
-    private List<FsStoreCartVO> carts;
+    private List<FsStoreCartStoreVO> carts;
 
 }

+ 1 - 1
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCartStoreVO.java

@@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
-public class FsStoreCartStoreVO extends FsStoreCartVO {
+public class FsStoreCartStoreVO extends FsStoreCartQueryVO {
 
     /** 店铺ID */
     private Long storeId;

+ 60 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreMultiStoreComputedVO.java

@@ -0,0 +1,60 @@
+package com.fs.hisStore.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 多店铺订单金额计算结果
+ *
+ * @author system
+ * @date 2026-08-10
+ */
+@Data
+public class FsStoreMultiStoreComputedVO implements Serializable {
+
+    @ApiModelProperty(value = "商品总金额(多店铺合计)")
+    private BigDecimal totalPrice;
+
+    @ApiModelProperty(value = "实际支付金额(多店铺合计)")
+    private BigDecimal payPrice;
+
+    @ApiModelProperty(value = "运费总金额")
+    private BigDecimal payPostage;
+
+    @ApiModelProperty(value = "优惠券抵扣金额")
+    private BigDecimal deductionPrice;
+
+    @ApiModelProperty(value = "使用积分")
+    private BigDecimal usedIntegral;
+
+    @ApiModelProperty(value = "抵扣金额(积分)")
+    private BigDecimal payIntegral;
+
+    @ApiModelProperty(value = "服务费总金额")
+    private BigDecimal serviceFee;
+
+    @ApiModelProperty(value = "各店铺费用明细")
+    private List<StoreComputedEntry> storeComputeds;
+
+    @Data
+    public static class StoreComputedEntry implements Serializable {
+        @ApiModelProperty(value = "店铺ID")
+        private Long storeId;
+
+        @ApiModelProperty(value = "店铺名称")
+        private String storeName;
+
+        @ApiModelProperty(value = "商品总金额")
+        private BigDecimal totalPrice;
+
+        @ApiModelProperty(value = "实际支付金额")
+        private BigDecimal payPrice;
+
+        @ApiModelProperty(value = "运费")
+        private BigDecimal payPostage;
+    }
+}

+ 39 - 0
java/fs-service/src/main/java/com/fs/hisStore/vo/FsStoreMultiStoreConfirmVO.java

@@ -0,0 +1,39 @@
+package com.fs.hisStore.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 多店铺确认订单结果
+ *
+ * @author system
+ * @date 2026-08-10
+ */
+@Data
+public class FsStoreMultiStoreConfirmVO implements Serializable {
+
+    @ApiModelProperty(value = "默认地址")
+    private Object address;
+
+    @ApiModelProperty(value = "各店铺确认结果")
+    private List<StoreConfirmEntry> storeConfirms;
+
+    @Data
+    public static class StoreConfirmEntry implements Serializable {
+        @ApiModelProperty(value = "店铺ID")
+        private Long storeId;
+
+        @ApiModelProperty(value = "店铺名称")
+        private String storeName;
+
+        @ApiModelProperty(value = "确认订单返回的orderKey,用于后续计算和下单")
+        private String orderKey;
+
+        @ApiModelProperty(value = "该店铺购物车商品列表")
+        private List<?> carts;
+    }
+}

+ 25 - 0
java/fs-service/src/main/java/com/fs/platform/domain/ResourceAuth.java

@@ -0,0 +1,25 @@
+package com.fs.platform.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import java.util.Date;
+
+@Data
+@TableName("resource_auth")
+public class ResourceAuth {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long tenantId;
+    private String resourceType;   // COURSE/LIVE/PRODUCT
+    private Long resourceId;
+    private String resourceName;
+    private Date authStartTime;
+    private Date authEndTime;
+    private Integer status;        // 0=未生效, 1=生效中, 2=已过期
+    private String remark;
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+    @TableField(fill = FieldFill.INSERT)
+    private String createBy;
+}

+ 7 - 0
java/fs-service/src/main/java/com/fs/platform/mapper/ResourceAuthMapper.java

@@ -0,0 +1,7 @@
+package com.fs.platform.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.platform.domain.ResourceAuth;
+
+public interface ResourceAuthMapper extends BaseMapper<ResourceAuth> {
+}

+ 13 - 0
java/fs-service/src/main/java/com/fs/platform/service/IResourceAuthService.java

@@ -0,0 +1,13 @@
+package com.fs.platform.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.platform.domain.ResourceAuth;
+
+import java.util.List;
+
+public interface IResourceAuthService extends IService<ResourceAuth> {
+
+    List<ResourceAuth> selectResourceAuthList(ResourceAuth auth);
+
+    boolean batchInsertResourceAuth(List<ResourceAuth> authList);
+}

+ 38 - 0
java/fs-service/src/main/java/com/fs/platform/service/impl/ResourceAuthServiceImpl.java

@@ -0,0 +1,38 @@
+package com.fs.platform.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.platform.domain.ResourceAuth;
+import com.fs.platform.mapper.ResourceAuthMapper;
+import com.fs.platform.service.IResourceAuthService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class ResourceAuthServiceImpl extends ServiceImpl<ResourceAuthMapper, ResourceAuth> implements IResourceAuthService {
+
+    @Override
+    public List<ResourceAuth> selectResourceAuthList(ResourceAuth auth) {
+        LambdaQueryWrapper<ResourceAuth> qw = new LambdaQueryWrapper<>();
+        if (auth.getTenantId() != null) {
+            qw.eq(ResourceAuth::getTenantId, auth.getTenantId());
+        }
+        if (auth.getResourceType() != null) {
+            qw.eq(ResourceAuth::getResourceType, auth.getResourceType());
+        }
+        if (auth.getStatus() != null) {
+            qw.eq(ResourceAuth::getStatus, auth.getStatus());
+        }
+        qw.orderByDesc(ResourceAuth::getCreateTime);
+        return this.list(qw);
+    }
+
+    @Override
+    public boolean batchInsertResourceAuth(List<ResourceAuth> authList) {
+        for (ResourceAuth auth : authList) {
+            auth.setStatus(1);
+        }
+        return this.saveBatch(authList);
+    }
+}

+ 60 - 0
java/fs-service/src/main/java/com/fs/proxy/domain/ProxyPurchaseSplit.java

@@ -0,0 +1,60 @@
+package com.fs.proxy.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 代理进货分成记录
+ */
+@Data
+@TableName("proxy_purchase_split")
+public class ProxyPurchaseSplit {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 代理ID */
+    private Long proxyId;
+
+    /** 租户ID */
+    private Long tenantId;
+
+    /** 租户名称 */
+    private String tenantName;
+
+    /** 进货订单号 */
+    private String orderCode;
+
+    /** 进货金额 */
+    private BigDecimal purchaseAmount;
+
+    /** 分成比例(%) */
+    private BigDecimal splitRatio;
+
+    /** 分成金额 */
+    private BigDecimal splitAmount;
+
+    /** 结算状态: 0-待结算, 1-已结算 */
+    private Integer settleStatus;
+
+    /** 结算时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime settleTime;
+
+    /** 结算月份 */
+    private String month;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 9 - 0
java/fs-service/src/main/java/com/fs/proxy/mapper/ProxyPurchaseSplitMapper.java

@@ -0,0 +1,9 @@
+package com.fs.proxy.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.proxy.domain.ProxyPurchaseSplit;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ProxyPurchaseSplitMapper extends BaseMapper<ProxyPurchaseSplit> {
+}

+ 21 - 0
java/fs-service/src/main/java/com/fs/proxy/service/IProxyPurchaseSplitService.java

@@ -0,0 +1,21 @@
+package com.fs.proxy.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.proxy.domain.ProxyPurchaseSplit;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+public interface IProxyPurchaseSplitService extends IService<ProxyPurchaseSplit> {
+
+    /**
+     * 查询进货分成列表(带条件)
+     */
+    List<ProxyPurchaseSplit> selectPurchaseSplitList(ProxyPurchaseSplit split);
+
+    /**
+     * 进货分成汇总统计
+     */
+    Map<String, Object> purchaseSplitSummary(Long proxyId, String month);
+}

+ 70 - 0
java/fs-service/src/main/java/com/fs/proxy/service/impl/ProxyPurchaseSplitServiceImpl.java

@@ -0,0 +1,70 @@
+package com.fs.proxy.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.proxy.domain.ProxyPurchaseSplit;
+import com.fs.proxy.mapper.ProxyPurchaseSplitMapper;
+import com.fs.proxy.service.IProxyPurchaseSplitService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ProxyPurchaseSplitServiceImpl extends ServiceImpl<ProxyPurchaseSplitMapper, ProxyPurchaseSplit>
+        implements IProxyPurchaseSplitService {
+
+    @Override
+    public List<ProxyPurchaseSplit> selectPurchaseSplitList(ProxyPurchaseSplit split) {
+        LambdaQueryWrapper<ProxyPurchaseSplit> qw = new LambdaQueryWrapper<>();
+        if (split.getProxyId() != null) {
+            qw.eq(ProxyPurchaseSplit::getProxyId, split.getProxyId());
+        }
+        if (split.getTenantId() != null) {
+            qw.eq(ProxyPurchaseSplit::getTenantId, split.getTenantId());
+        }
+        if (split.getTenantName() != null && !split.getTenantName().isEmpty()) {
+            qw.like(ProxyPurchaseSplit::getTenantName, split.getTenantName());
+        }
+        if (split.getOrderCode() != null && !split.getOrderCode().isEmpty()) {
+            qw.like(ProxyPurchaseSplit::getOrderCode, split.getOrderCode());
+        }
+        if (split.getSettleStatus() != null) {
+            qw.eq(ProxyPurchaseSplit::getSettleStatus, split.getSettleStatus());
+        }
+        if (split.getMonth() != null && !split.getMonth().isEmpty()) {
+            qw.eq(ProxyPurchaseSplit::getMonth, split.getMonth());
+        }
+        qw.orderByDesc(ProxyPurchaseSplit::getCreateTime);
+        return this.list(qw);
+    }
+
+    @Override
+    public Map<String, Object> purchaseSplitSummary(Long proxyId, String month) {
+        Map<String, Object> result = new HashMap<>();
+        LambdaQueryWrapper<ProxyPurchaseSplit> qw = new LambdaQueryWrapper<>();
+        qw.eq(ProxyPurchaseSplit::getProxyId, proxyId);
+        if (month != null && !month.isEmpty()) {
+            qw.eq(ProxyPurchaseSplit::getMonth, month);
+        }
+        List<ProxyPurchaseSplit> list = this.list(qw);
+
+        BigDecimal totalSplitAmount = list.stream()
+                .map(ProxyPurchaseSplit::getSplitAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        long pendingCount = list.stream()
+                .filter(s -> s.getSettleStatus() != null && s.getSettleStatus() == 0)
+                .count();
+        long settledCount = list.stream()
+                .filter(s -> s.getSettleStatus() != null && s.getSettleStatus() == 1)
+                .count();
+
+        result.put("totalSplitAmount", totalSplitAmount);
+        result.put("pendingCount", pendingCount);
+        result.put("settledCount", settledCount);
+        result.put("totalCount", list.size());
+        return result;
+    }
+}

+ 7 - 0
java/fs-service/src/main/java/com/fs/tenant/domain/TenantInfo.java

@@ -108,6 +108,13 @@ public class TenantInfo {
 
     private String billingMode;
 
+    /** 租户订阅套餐ID */
+    private Long packageId;
+
+    /** 套餐名称(非数据库字段) */
+    @TableField(exist = false)
+    private String packageName;
+
     @TableField(exist = false)
     @com.fasterxml.jackson.annotation.JsonProperty("userName")
     private String adminUserName;

+ 27 - 0
java/fs-service/src/main/java/com/fs/tenant/domain/TenantPackage.java

@@ -0,0 +1,27 @@
+package com.fs.tenant.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("tenant_package")
+public class TenantPackage extends BaseEntity {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String packageName;
+    private String packageCode;
+    private BigDecimal price;
+    private Integer durationDays;
+    private String modules;
+    private Integer maxStoreCount;
+    private Integer maxStaffCount;
+    private Integer maxDoctorCount;
+    private Integer status;
+    private Integer sort;
+    private String remark;
+}

+ 7 - 0
java/fs-service/src/main/java/com/fs/tenant/mapper/TenantPackageMapper.java

@@ -0,0 +1,7 @@
+package com.fs.tenant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.tenant.domain.TenantPackage;
+
+public interface TenantPackageMapper extends BaseMapper<TenantPackage> {
+}

+ 11 - 0
java/fs-service/src/main/java/com/fs/tenant/service/ITenantPackageService.java

@@ -0,0 +1,11 @@
+package com.fs.tenant.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.tenant.domain.TenantPackage;
+
+import java.util.List;
+
+public interface ITenantPackageService extends IService<TenantPackage> {
+
+    List<TenantPackage> selectTenantPackageList(TenantPackage pkg);
+}

+ 27 - 0
java/fs-service/src/main/java/com/fs/tenant/service/impl/TenantPackageServiceImpl.java

@@ -0,0 +1,27 @@
+package com.fs.tenant.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.tenant.domain.TenantPackage;
+import com.fs.tenant.mapper.TenantPackageMapper;
+import com.fs.tenant.service.ITenantPackageService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class TenantPackageServiceImpl extends ServiceImpl<TenantPackageMapper, TenantPackage> implements ITenantPackageService {
+
+    @Override
+    public List<TenantPackage> selectTenantPackageList(TenantPackage pkg) {
+        LambdaQueryWrapper<TenantPackage> qw = new LambdaQueryWrapper<>();
+        if (pkg.getPackageName() != null) {
+            qw.like(TenantPackage::getPackageName, pkg.getPackageName());
+        }
+        if (pkg.getStatus() != null) {
+            qw.eq(TenantPackage::getStatus, pkg.getStatus());
+        }
+        qw.orderByAsc(TenantPackage::getSort);
+        return this.list(qw);
+    }
+}

+ 33 - 0
java/fs-service/src/main/java/com/fs/tenant/task/TenantExpireCheckTask.java

@@ -0,0 +1,33 @@
+package com.fs.tenant.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import java.util.Date;
+import java.util.List;
+
+@Slf4j
+@Component
+public class TenantExpireCheckTask {
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+
+    @Scheduled(cron = "0 0 0 * * ?") // 每日0点
+    public void checkExpiredTenants() {
+        log.info("开始检查租户到期状态...");
+        LambdaQueryWrapper<TenantInfo> qw = new LambdaQueryWrapper<>();
+        qw.le(TenantInfo::getExpireTime, new Date());
+        qw.eq(TenantInfo::getStatus, 1);
+        List<TenantInfo> expiredList = tenantInfoMapper.selectList(qw);
+        for (TenantInfo tenant : expiredList) {
+            log.warn("租户[{}]已到期,到期时间: {}", tenant.getTenantCode(), tenant.getExpireTime());
+            tenant.setStatus(0);
+            tenantInfoMapper.updateById(tenant);
+        }
+        log.info("租户到期检查完成,共处理{}条到期记录", expiredList.size());
+    }
+}

+ 80 - 8
java/fs-service/src/main/resources/db/tenant-initData.sql

@@ -5735,12 +5735,84 @@ VALUES (1, '开始节点', 'start', '自动生成触发条件和变量初始化'
        (52, '商品推送', 'product_push', '推送商品小程序地址', 'extended', 52),
        (53, '物流推送', 'logistics_notify', '推送物流信息', 'extended', 53),
        (100, '外部API', 'external_api', '调用外部API接口', 'external', 100);
--- 3. 插入系统菜单(代理管理)
+-- . 插入系统菜单(代理管理 / admin:proxy:* 权限体系)
+--    父菜单
 INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES
-('代理管理', 0, 2, 'agent', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'user', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '代理管理目录'),
-('代理列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '代理管理' AND parent_id = 0) AS t), 1, 'list', 'agent/index', NULL, 1, 0, 'C', '0', '0', 'agent:agent:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '代理列表菜单'),
-('代理新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '代理管理' AND parent_id = 0) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'agent:agent:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
-('代理编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '代理管理' AND parent_id = 0) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'agent:agent:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
-('代理删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '代理管理' AND parent_id = 0) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'agent:agent:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
-('代理导出', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '代理管理' AND parent_id = 0) AS t), 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'agent:agent:export', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''),
-('代理查询', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name = '代理管理' AND parent_id = 0) AS t), 6, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'agent:agent:query', '#', b'1', b'0', NULL, 6, NULL, 'admin', now(), '', now(), '');
+('代理管理', 0, 2, 'proxy', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'user', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '代理管理目录');
+
+--    子菜单 - 代理列表
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理列表', m.menu_id, 1, 'list', 'admin/proxy/index', NULL, 1, 0, 'C', '0', '0', 'admin:proxy:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '代理列表菜单'
+FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
+
+--    按钮 - 新增
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理新增', m.menu_id, 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''
+FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
+
+--    按钮 - 编辑
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理编辑', m.menu_id, 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''
+FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
+
+--    按钮 - 删除
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理删除', m.menu_id, 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''
+FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
+
+--    按钮 - 查询(用于详情)
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理查询', m.menu_id, 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:query', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''
+FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
+-- 新增菜单
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES
+-- 供应商管理
+('供应商管理', 0, 5, 'supplier', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'tree', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), '供应商管理目录'),
+('供应商列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/supplier/index', NULL, 1, 0, 'C', '0', '0', 'admin:supplier:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), ''),
+('供应商新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
+('供应商编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
+('供应商删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
+('供应商查询', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:query', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''),
+-- 设备类型管理
+('设备类型管理', 0, 6, 'deviceType', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'component', b'1', b'0', NULL, 6, NULL, 'admin', now(), '', now(), '设备类型管理目录'),
+('设备类型列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/deviceType/index', NULL, 1, 0, 'C', '0', '0', 'admin:deviceType:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), ''),
+('设备类型新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
+('设备类型编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
+('设备类型删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
+-- 设备厂商管理
+('设备厂商管理', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 5, 'manufacturer', 'admin/deviceManufacturer/index', NULL, 1, 0, 'C', '0', '0', 'admin:deviceManufacturer:list', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''),
+('设备厂商新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 6, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:add', '#', b'1', b'0', NULL, 6, NULL, 'admin', now(), '', now(), ''),
+('设备厂商编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 7, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:edit', '#', b'1', b'0', NULL, 7, NULL, 'admin', now(), '', now(), ''),
+('设备厂商删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 8, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:remove', '#', b'1', b'0', NULL, 8, NULL, 'admin', now(), '', now(), ''),
+-- 租户套餐管理
+('套餐管理', 0, 3, 'tenantPackage', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'peoples', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), '套餐管理目录'),
+('套餐列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/tenantPackage/index', NULL, 1, 0, 'C', '0', '0', 'admin:tenantPackage:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), ''),
+('套餐新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
+('套餐编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
+('套餐删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
+-- 财务总账
+('财务管理', 0, 8, 'finance', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'money', b'1', b'0', NULL, 8, NULL, 'admin', now(), '', now(), '财务管理目录'),
+('财务总账', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='财务管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'ledger', 'admin/finance/ledger', NULL, 1, 0, 'C', '0', '0', 'admin:finance:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '');
+-- 进货分成菜单
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES ('进货分成', 0, 9, 'purchaseSplit', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'el-icon-s-cooperation', 'admin', NOW(), '代理端进货分成查询');
+
+-- 获取上述插入的 menu_id 用于子菜单
+SET @purchase_split_menu_id = LAST_INSERT_ID();
+
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES ('进货分成查询', @purchase_split_menu_id, 1, 'list', 'purchaseSplit/index', NULL, 1, 0, 'C', '0', '0', 'proxy:purchaseSplit:list', 'el-icon-document', 'admin', NOW(), '查询进货分成列表');
+
+-- 消息通知菜单
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES ('消息通知', 0, 10, 'notice', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'el-icon-bell', 'admin', NOW(), '代理端消息通知');
+
+SET @notice_menu_id = LAST_INSERT_ID();
+
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES ('公告通知', @notice_menu_id, 1, 'list', 'notice/index', NULL, 1, 0, 'C', '0', '0', 'system:notice:list', 'el-icon-message', 'admin', NOW(), '查看公告通知');
+
+-- 租户套餐分配权限(使用已有租户管理菜单,仅新增权限)
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
+VALUES ('租户套餐分配', NULL, 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'proxy:tenant:package', NULL, 'admin', NOW(), '为租户分配套餐权限');
+-- 注: parent_id 应设置为代理端租户管理菜单的 id

+ 212 - 1
java/fs-service/src/main/resources/db/tenant-initTable.sql

@@ -16803,6 +16803,7 @@ CREATE TABLE `tenant_info` (
    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
    `contact_name` varchar(50) DEFAULT NULL COMMENT '联系人',
+   `package_id` BIGINT(20) DEFAULT NULL COMMENT '租户订阅套餐ID' ,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_code` (`tenant_code`),
    KEY `idx_status` (`status`) USING BTREE
@@ -21495,4 +21496,214 @@ CREATE TABLE `agent_wallet_txn` (
     KEY `idx_agent_id`    (`agent_id`),
     KEY `idx_txn_type`    (`txn_type`),
     KEY `idx_create_time` (`create_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理账户流水表';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理账户流水表';
+-- 1. 代理信息表 (对应 com.fs.proxy.domain.Proxy)
+CREATE TABLE IF NOT EXISTS `proxy` (
+    `proxy_id`           BIGINT(20)     NOT NULL AUTO_INCREMENT   COMMENT '代理ID',
+    `proxy_name`         VARCHAR(100)   NOT NULL                  COMMENT '代理名称',
+    `password`           VARCHAR(255)   DEFAULT NULL              COMMENT '登录密码',
+    `contact_name`       VARCHAR(50)    DEFAULT NULL              COMMENT '联系人',
+    `contact_mobile`     VARCHAR(20)    DEFAULT NULL              COMMENT '联系电话',
+    `email`              VARCHAR(100)   DEFAULT NULL              COMMENT '邮箱',
+    `company_name`       VARCHAR(200)   DEFAULT NULL              COMMENT '公司名称',
+    `status`             INT(11)        DEFAULT 1                 COMMENT '状态(0=禁用, 1=启用)',
+    `balance`            DECIMAL(12,2)  DEFAULT 0.00             COMMENT '账户余额(元)',
+    `frozen_amount`      DECIMAL(12,2)  DEFAULT 0.00             COMMENT '冻结金额(元)',
+    `profit_share_ratio` DECIMAL(5,2)   DEFAULT 0.00             COMMENT '分账比例(%)',
+    `account_fee`        DECIMAL(12,2)  DEFAULT 0.00             COMMENT '开户费(元)',
+    `open_time`          DATETIME       DEFAULT NULL              COMMENT '开户时间',
+    `expire_time`        DATETIME       DEFAULT NULL              COMMENT '到期时间',
+    `remark`             VARCHAR(500)   DEFAULT NULL              COMMENT '备注',
+    `is_del`             INT(11)        DEFAULT 0                 COMMENT '是否删除(0=否, 1=是)',
+    `create_by`          VARCHAR(64)    DEFAULT NULL              COMMENT '创建者',
+    `create_time`        DATETIME       DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_by`          VARCHAR(64)    DEFAULT NULL              COMMENT '更新者',
+    `update_time`        DATETIME       DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`proxy_id`),
+    KEY `idx_proxy_name`   (`proxy_name`),
+    KEY `idx_contact_mobile` (`contact_mobile`),
+    KEY `idx_status`       (`status`),
+    KEY `idx_create_time`  (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理信息表';
+
+
+-- 2. 代理操作日志表 (对应 com.fs.proxy.domain.ProxyOperLog)
+CREATE TABLE IF NOT EXISTS `proxy_oper_log` (
+    `oper_id`         BIGINT(20)    NOT NULL AUTO_INCREMENT   COMMENT '日志主键',
+    `proxy_id`        BIGINT(20)    DEFAULT NULL              COMMENT '代理ID',
+    `proxy_name`      VARCHAR(100)  DEFAULT NULL              COMMENT '代理名称',
+    `title`           VARCHAR(50)   DEFAULT NULL              COMMENT '模块标题',
+    `business_type`   INT(11)       DEFAULT NULL              COMMENT '业务类型(0=其它, 1=新增, 2=修改, 3=删除)',
+    `method`          VARCHAR(100)  DEFAULT NULL              COMMENT '方法名称',
+    `request_method`  VARCHAR(10)   DEFAULT NULL              COMMENT '请求方式(GET/POST/PUT/DELETE)',
+    `operator_type`   INT(11)       DEFAULT NULL              COMMENT '操作类别(0=其它, 1=后台用户)',
+    `oper_name`       VARCHAR(50)   DEFAULT NULL              COMMENT '操作人员',
+    `dept_name`       VARCHAR(50)   DEFAULT NULL              COMMENT '部门名称',
+    `oper_url`        VARCHAR(2000) DEFAULT NULL              COMMENT '请求URL',
+    `oper_ip`         VARCHAR(50)   DEFAULT NULL              COMMENT '主机地址',
+    `oper_location`   VARCHAR(255)  DEFAULT NULL              COMMENT '操作地点',
+    `oper_param`      LONGTEXT      DEFAULT NULL              COMMENT '请求参数',
+    `json_result`     VARCHAR(2000) DEFAULT NULL              COMMENT '返回参数',
+    `status`          INT(11)       DEFAULT NULL              COMMENT '操作状态(0=正常, 1=异常)',
+    `error_msg`       VARCHAR(2000) DEFAULT NULL              COMMENT '错误消息',
+    `oper_time`       DATETIME      DEFAULT NULL              COMMENT '操作时间',
+    PRIMARY KEY (`oper_id`),
+    KEY `idx_proxy_id`   (`proxy_id`),
+    KEY `idx_oper_time`  (`oper_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代理操作日志表';
+-- 供应商管理表 (如果不存在)
+CREATE TABLE IF NOT EXISTS `oms_supplier` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `code` varchar(64) DEFAULT NULL COMMENT '供应商编码',
+    `name` varchar(100) NOT NULL COMMENT '供应商名称',
+    `type` varchar(32) DEFAULT NULL COMMENT '供应商分类',
+    `level` varchar(32) DEFAULT NULL COMMENT '供应商等级',
+    `cycle_day` bigint DEFAULT NULL COMMENT '送货周期(天)',
+    `invoice_type` bigint DEFAULT 0 COMMENT '开票类型 0无 1电子 2普票 3专票',
+    `overfill_rate` decimal(5,2) DEFAULT NULL COMMENT '溢装比例',
+    `tax_rate` decimal(5,2) DEFAULT NULL COMMENT '税率',
+    `payment_type` varchar(32) DEFAULT NULL COMMENT '结算方式',
+    `full_name` varchar(200) DEFAULT NULL COMMENT '公司全称',
+    `mnemonic` varchar(32) DEFAULT NULL COMMENT '助记码',
+    `cooperation_time` varchar(32) DEFAULT NULL COMMENT '合作日期',
+    `contact_name` varchar(50) DEFAULT NULL COMMENT '联系人',
+    `contract_tel` varchar(20) DEFAULT NULL COMMENT '联系人电话',
+    `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系人手机',
+    `city_ids` varchar(255) DEFAULT NULL COMMENT '省市区',
+    `address` varchar(255) DEFAULT NULL COMMENT '详细地址',
+    `bank_name` varchar(100) DEFAULT NULL COMMENT '账户名称',
+    `bank_deposit` varchar(100) DEFAULT NULL COMMENT '开户银行',
+    `bank_account` varchar(64) DEFAULT NULL COMMENT '银行账户',
+    `status` bigint DEFAULT 1 COMMENT '状态 1启用 0关闭',
+    `remark` varchar(500) DEFAULT NULL,
+    `create_user_id` bigint DEFAULT NULL,
+    `update_user_id` bigint DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `create_by` varchar(64) DEFAULT NULL,
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    `update_by` varchar(64) DEFAULT NULL,
+    PRIMARY KEY (`id`),
+    KEY `idx_name` (`name`),
+    KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商管理表';
+
+-- 设备类型表
+CREATE TABLE IF NOT EXISTS `device_type` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `type_name` varchar(100) NOT NULL COMMENT '类型名称',
+    `parent_id` bigint DEFAULT 0 COMMENT '父类型ID',
+    `description` varchar(255) DEFAULT NULL,
+    `status` int DEFAULT 0 COMMENT '0启用 1停用',
+    `remark` varchar(500) DEFAULT NULL,
+    `create_by` varchar(64) DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `update_by` varchar(64) DEFAULT NULL,
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备类型表';
+
+-- 设备厂商表
+CREATE TABLE IF NOT EXISTS `device_manufacturer` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `name` varchar(100) NOT NULL COMMENT '厂商名称',
+    `credit_code` varchar(64) DEFAULT NULL COMMENT '信用代码',
+    `contact_person` varchar(50) DEFAULT NULL,
+    `contact_phone` varchar(20) DEFAULT NULL,
+    `email` varchar(100) DEFAULT NULL,
+    `address` varchar(255) DEFAULT NULL,
+    `main_category` varchar(100) DEFAULT NULL COMMENT '主营品类',
+    `bank_account` varchar(64) DEFAULT NULL,
+    `bank_name` varchar(100) DEFAULT NULL COMMENT '开户行',
+    `rating` int DEFAULT 0 COMMENT '评级1-5',
+    `status` int DEFAULT 0 COMMENT '0合作中 1已停止',
+    `license_url` varchar(255) DEFAULT NULL COMMENT '营业执照',
+    `remark` varchar(500) DEFAULT NULL,
+    `create_by` varchar(64) DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `update_by` varchar(64) DEFAULT NULL,
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备厂商表';
+
+-- 租户订阅套餐表
+CREATE TABLE IF NOT EXISTS `tenant_package` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `package_name` varchar(100) NOT NULL COMMENT '套餐名称',
+    `package_code` varchar(64) NOT NULL COMMENT '套餐编码',
+    `price` decimal(10,2) DEFAULT 0 COMMENT '套餐价格(元)',
+    `duration_days` int DEFAULT 365 COMMENT '有效天数',
+    `modules` text COMMENT '功能模块(JSON数组)',
+    `max_store_count` int DEFAULT 0 COMMENT '最大门店数',
+    `max_staff_count` int DEFAULT 0 COMMENT '最大员工数',
+    `max_doctor_count` int DEFAULT 0 COMMENT '最大医生数',
+    `status` int DEFAULT 1 COMMENT '0停用 1启用',
+    `sort` int DEFAULT 0 COMMENT '排序',
+    `remark` varchar(500) DEFAULT NULL,
+    `create_by` varchar(64) DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `update_by` varchar(64) DEFAULT NULL,
+    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_code` (`package_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户订阅套餐表';
+
+-- 平台资源授权表
+CREATE TABLE IF NOT EXISTS `resource_auth` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `tenant_id` bigint NOT NULL COMMENT '租户ID',
+    `resource_type` varchar(32) NOT NULL COMMENT '资源类型 COURSE/LIVE/PRODUCT',
+    `resource_id` bigint NOT NULL COMMENT '资源ID',
+    `resource_name` varchar(200) DEFAULT NULL COMMENT '资源名称',
+    `auth_start_time` datetime DEFAULT NULL COMMENT '授权开始时间',
+    `auth_end_time` datetime DEFAULT NULL COMMENT '授权结束时间',
+    `status` int DEFAULT 1 COMMENT '0未生效 1生效中 2已过期',
+    `remark` varchar(500) DEFAULT NULL,
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `create_by` varchar(64) DEFAULT NULL,
+    PRIMARY KEY (`id`),
+    KEY `idx_tenant` (`tenant_id`),
+    KEY `idx_resource` (`resource_type`, `resource_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台资源授权表';
+
+-- 财务总账表
+CREATE TABLE IF NOT EXISTS `financial_ledger` (
+    `id` bigint NOT NULL AUTO_INCREMENT,
+    `order_type` varchar(32) NOT NULL COMMENT '业务类型 DISTRIBUTION/STOCK/SERVICE',
+    `order_id` bigint DEFAULT NULL COMMENT '关联订单ID',
+    `order_code` varchar(64) DEFAULT NULL COMMENT '关联订单号',
+    `from_account` varchar(32) DEFAULT NULL COMMENT '支出账户类型',
+    `from_id` bigint DEFAULT NULL COMMENT '支出账户ID',
+    `to_account` varchar(32) DEFAULT NULL COMMENT '收入账户类型',
+    `to_id` bigint DEFAULT NULL COMMENT '收入账户ID',
+    `amount` decimal(12,2) NOT NULL COMMENT '金额',
+    `fee` decimal(12,2) DEFAULT 0.00 COMMENT '手续费',
+    `status` int DEFAULT 0 COMMENT '0待入账 1已入账',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
+    `remark` varchar(500) DEFAULT NULL,
+    PRIMARY KEY (`id`),
+    KEY `idx_order_type` (`order_type`),
+    KEY `idx_status` (`status`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='财务总账表';
+
+DROP TABLE IF EXISTS `proxy_purchase_split`;
+CREATE TABLE `proxy_purchase_split` (
+                                        `id`           BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+                                        `proxy_id`     BIGINT(20)   NOT NULL                COMMENT '代理ID',
+                                        `tenant_id`    BIGINT(20)   NOT NULL                COMMENT '租户ID',
+                                        `tenant_name`  VARCHAR(100) DEFAULT ''               COMMENT '租户名称',
+                                        `order_code`   VARCHAR(64)  DEFAULT ''               COMMENT '进货订单号',
+                                        `purchase_amount` DECIMAL(18,2) DEFAULT 0.00         COMMENT '进货金额',
+                                        `split_ratio`  DECIMAL(10,2) DEFAULT 0.00            COMMENT '分成比例(%)',
+                                        `split_amount` DECIMAL(18,2) DEFAULT 0.00            COMMENT '分成金额',
+                                        `settle_status` TINYINT(1)  DEFAULT 0                COMMENT '结算状态: 0-待结算, 1-已结算',
+                                        `settle_time`  DATETIME     DEFAULT NULL             COMMENT '结算时间',
+                                        `month`        VARCHAR(7)   DEFAULT NULL             COMMENT '结算月份(yyyy-MM)',
+                                        `create_time`  DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+                                        `update_time`  DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+                                        PRIMARY KEY (`id`),
+                                        INDEX `idx_proxy_id` (`proxy_id`),
+                                        INDEX `idx_tenant_id` (`tenant_id`),
+                                        INDEX `idx_settle_status` (`settle_status`),
+                                        INDEX `idx_month` (`month`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代理进货分成记录表';

+ 11 - 0
java/fs-service/src/main/resources/mapper/hisStore/FsStoreCartScrmMapper.xml

@@ -117,4 +117,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         and c.user_id = #{userId}
     </select>
 
+    <select id="selectMultiStoreCartCount" resultType="Integer">
+        select ifnull(sum(c.cart_num), 0) from fs_store_cart_scrm c
+        where c.is_pay = 0 and c.is_del = 0 and c.is_buy = 0
+        <if test='param.userId != null'>
+            and c.user_id = #{param.userId}
+        </if>
+        <if test="param.storeId != null and param.storeId != ''">
+            and c.store_id = #{param.storeId}
+        </if>
+    </select>
+
 </mapper>

+ 15 - 2
java/fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -2286,17 +2286,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         SELECT
             c.id,
             c.product_id           AS productId,
+            c.product_attr_value_id AS productAttrValueId,
             c.cart_num             AS cartNum,
             p.store_id             AS storeId,
             s.store_name           AS storeName,
             p.product_name         AS productName,
             p.image                AS productImage,
             p.price                AS price,
-            p.stock                AS stock,
-            p.product_attr_name    AS productAttrName
+            p.product_type         AS productType,
+            v.sku                  AS productAttrName,
+            v.image                AS productAttrImage,
+            v.stock                AS stock,
+            v.cost                 AS cost,
+            v.integral             AS integral,
+            v.weight               AS weight,
+            v.volume               AS volume,
+            v.bar_code             AS barCode,
+            v.group_bar_code       AS groupBarCode,
+            v.brokerage            AS brokerage,
+            v.brokerage_two        AS brokerageTwo,
+            v.brokerage_three      AS brokerageThree
         FROM fs_store_cart_scrm c
         INNER JOIN fs_store_product_scrm p ON c.product_id = p.product_id
         LEFT JOIN fs_store_scrm s ON p.store_id = s.store_id
+        left join fs_store_product_attr_value_scrm v on v.id=c.product_attr_value_id
         WHERE c.id IN
         <foreach collection="cartIds" item="id" open="(" separator="," close=")">
             #{id}

+ 21 - 10
java/fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

@@ -407,13 +407,14 @@ public class ProductScrmController extends AppBaseController {
     @GetMapping("/getCartsWithStore")
     public R getCartsWithStore(HttpServletRequest request){
         Long userId = Long.parseLong(getUserId());
-        String cacheKey = buildAllCartKey(userId);
+//        String cacheKey = buildAllCartKey(userId);
         @SuppressWarnings("unchecked")
-        List<FsStoreCartStoreVO> carts = redisCache.getCacheObject(cacheKey);
-        if (carts == null) {
+        List<FsStoreCartStoreVO>
+//                carts = redisCache.getCacheObject(cacheKey);
+//        if (carts == null) {
             carts = cartService.selectFsStoreCartStoreListByUid(userId);
-            redisCache.setCacheObject(cacheKey, carts, CART_CACHE_TTL, TimeUnit.SECONDS);
-        }
+//            redisCache.setCacheObject(cacheKey, carts, CART_CACHE_TTL, TimeUnit.SECONDS);
+//        }
         // 按 storeId 分组
         Map<Long, List<FsStoreCartStoreVO>> groupedMap = carts.stream()
                 .collect(java.util.stream.Collectors.groupingBy(
@@ -437,13 +438,14 @@ public class ProductScrmController extends AppBaseController {
     @GetMapping("/getCartsByStore")
     public R getCartsByStore(@RequestParam("storeId") Long storeId, HttpServletRequest request){
         Long userId = Long.parseLong(getUserId());
-        String cacheKey = buildStoreCartKey(storeId, userId);
+//        String cacheKey = buildStoreCartKey(storeId, userId);
         @SuppressWarnings("unchecked")
-        List<FsStoreCartStoreVO> carts = redisCache.getCacheObject(cacheKey);
-        if (carts == null) {
+        List<FsStoreCartStoreVO>
+//                carts = redisCache.getCacheObject(cacheKey);
+//        if (carts == null) {
             carts = cartService.selectFsStoreCartListByUidAndStoreId(userId, storeId);
-            redisCache.setCacheObject(cacheKey, carts, CART_CACHE_TTL, TimeUnit.SECONDS);
-        }
+//            redisCache.setCacheObject(cacheKey, carts, CART_CACHE_TTL, TimeUnit.SECONDS);
+//        }
         return R.ok().put("carts", carts);
     }
     @Login
@@ -496,6 +498,15 @@ public class ProductScrmController extends AppBaseController {
         return R.ok().put("data", count);
     }
 
+    @Login
+    @ApiOperation("获取多店铺商品购物车数量")
+    @PostMapping("/getCartCountByStore")
+    public R getCartCountByStore(@RequestBody FsStoreMultiCartCountParam param){
+        param.setUserId(Long.parseLong(getUserId()));
+        Integer count = cartService.selectMultiStoreCartCount(param);
+        return R.ok().put("data", count);
+    }
+
     @ApiOperation("获取推荐商品列表")
     @GetMapping("/getTuiProduct")
     public R getTuiProduct(HttpServletRequest request){

+ 18 - 1
java/fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

@@ -221,9 +221,26 @@ public class StoreOrderScrmController extends AppBaseController {
     @PostMapping("/createMultiStore")
     public R createMultiStore(@Validated @RequestBody FsStoreMultiStoreOrderCreateParam param, HttpServletRequest request){
         String userId = getUserId();
-        log.info("开始合并多店铺下单, 用户ID: {}, 店铺数: {}", userId, param.getStoreCarts().size());
+        log.info("开始合并多店铺下单, 用户ID: {}, orderKeys: {}", userId, param.getOrderKeys());
         return orderService.createMultiStoreOrder(Long.parseLong(userId), param);
     }
+    @Login
+    @ApiOperation("多店铺确认订单")
+    @PostMapping("/confirmMultiStore")
+    public R confirmMultiStore(@Validated @RequestBody FsStoreConfirmMultiStoreParam param, HttpServletRequest request){
+        String userId = getUserId();
+        log.info("多店铺确认订单, 用户ID: {}, cartIds: {}", userId, param.getCartIds());
+        return orderService.confirmMultiStore(Long.parseLong(userId), param);
+    }
+
+    @Login
+    @ApiOperation("多店铺计算订单金额")
+    @PostMapping("/computedMultiStore")
+    public R computedMultiStore(@Validated @RequestBody FsStoreOrderMultiStoreComputedParam param, HttpServletRequest request){
+        String userId = getUserId();
+        log.info("多店铺计算订单金额, 用户ID: {}, orderKey数: {}", userId, param.getOrderKeys().size());
+        return orderService.computedMultiStore(Long.parseLong(userId), param);
+    }
 
     @Login
     @ApiOperation("支付")