Ver Fonte

Merge branch 'master' of http://47.100.37.243:10191/wutt/manHourHousekeeper

QuYueTing há 1 mês atrás
pai
commit
36da32714b
21 ficheiros alterados com 1456 adições e 34 exclusões
  1. 4 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/team/api.ts
  2. 214 26
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/team/index.vue
  3. 53 5
      fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/request.ts
  4. 5 0
      fhKeeper/formulahousekeeper/management-crm/pom.xml
  5. 21 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/UserQrCodeController.java
  6. 420 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatAccountController.java
  7. 182 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WeiXinTicketController.java
  8. 69 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/UserQrCode.java
  9. 89 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WechatAccount.java
  10. 18 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WeiXinTicketEntity.java
  11. 16 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/UserQrCodeMapper.java
  12. 31 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/WechatAccountMapper.java
  13. 16 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/UserQrCodeService.java
  14. 16 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/WechatAccountService.java
  15. 20 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/UserQrCodeServiceImpl.java
  16. 14 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/UserServiceImpl.java
  17. 99 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatAccountServiceImpl.java
  18. 115 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/util/FileZipUtil.java
  19. 12 3
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/application.yml
  20. 20 0
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/UserQrCodeMapper.xml
  21. 22 0
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/WechatAccountMapper.xml

+ 4 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/team/api.ts

@@ -16,3 +16,7 @@ export const DEACTIVEUSER = `/user/deactiveUser`
 export const SETACTIVE = `/user/setActive`
 export const EXPOERTUSER = `/user/exportUsers`
 export const URL_IMPORTUSER = `/user/importUser`
+export const GET_COMPANY_WEI_XIN = `/wechat-account/getCompanyWeiXin`
+export const SAVE_OR_UPDATE = `/wechat-account/saveOrUpdate`
+export const ONE_CLICK_GENERATION = `/wechat-account/oneClickGeneration`
+export const EXPORT_QR_CODE = `/wechat-account/export`

+ 214 - 26
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/team/index.vue

@@ -28,7 +28,7 @@
             <el-button :icon="Search" @click="getTableData()" />
           </template>
         </el-input>
-        
+
         <div class="formItem mr-6 flex items-center">
           <div class="text-nowrap">状态:</div>
           <el-select v-model="teamForm.status" placeholder="请选择" size="default" style="width: 100px"
@@ -43,8 +43,10 @@
             <el-option v-for="item in roleList" :key="item.id" :label="item.rolename" :value="item.id" />
           </el-select>
         </div>
-        <el-button type="primary" v-if="userInfo.userNameNeedTranslate == 1" @click="transitionOperation('exportUser', '')" v-permission="['teamExport']">导出人员</el-button>
-        <el-button type="primary" v-if="userInfo.userNameNeedTranslate == 1" @click="dialogFrom.newSyncWithCorpWxDayloadVisable = true">同步企微通讯录</el-button>
+        <el-button type="primary" v-if="userInfo.userNameNeedTranslate == 1"
+          @click="transitionOperation('exportUser', '')" v-permission="['teamExport']">导出人员</el-button>
+        <el-button type="primary" v-if="userInfo.userNameNeedTranslate == 1"
+          @click="dialogFrom.newSyncWithCorpWxDayloadVisable = true">同步企微通讯录</el-button>
         <el-dropdown v-if="userInfo.userNameNeedTranslate != 1">
           <el-button type="primary">
             更多操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
@@ -52,8 +54,19 @@
           <template #dropdown>
             <el-dropdown-menu>
               <el-dropdown-item @click="addPersone(false)" v-permission="['teamAdd']">添加人员</el-dropdown-item>
-              <el-dropdown-item @click="transitionOperation('exportUser', '')" v-permission="['teamExport']">导出人员</el-dropdown-item>
-              <el-dropdown-item @click="transitionOperation('importUser', '')" v-permission="['teamImport']">批量导入</el-dropdown-item>
+              <el-dropdown-item @click="transitionOperation('exportUser', '')"
+                v-permission="['teamExport']">导出人员</el-dropdown-item>
+              <el-dropdown-item @click="transitionOperation('importUser', '')"
+                v-permission="['teamImport']">批量导入</el-dropdown-item>
+              <el-dropdown-item @click="officialAccountSetting()">
+                {{ officialAccountInformation.id ? '修改公众号配置' : '配置公众号' }}
+              </el-dropdown-item>
+              <el-dropdown-item @click="oneClickGenerationOfQrCode()" v-if="officialAccountInformation.id">
+                一键生成员工二维码
+              </el-dropdown-item>
+              <el-dropdown-item @click="exportQrCode()" v-if="officialAccountInformation.id">
+                导出员工二维码
+              </el-dropdown-item>
             </el-dropdown-menu>
           </template>
         </el-dropdown>
@@ -67,11 +80,17 @@
             <el-tree style="max-width: 600px" :data="deptList" :props="treeProps" @node-click="treeNode">
               <template #default="{ node, data }">
                 <div class="flex justify-between treeContent">
-                  <div class="custom-tree-node" @mouseleave="mouseleave(data,$event)" @mouseover="mouseover(data,$event)">
-                    <div class="treeLabel"><TextTranslation translationTypes="departmentName" :translationValue="node.label"></TextTranslation></div>
+                  <div class="custom-tree-node" @mouseleave="mouseleave(data, $event)"
+                    @mouseover="mouseover(data, $event)">
+                    <div class="treeLabel">
+                      <TextTranslation translationTypes="departmentName" :translationValue="node.label">
+                      </TextTranslation>
+                    </div>
                     <div class="treeIcon nodeEle" id="treeIcon" v-if="data.id > 0">
-                      <el-link type="primary" :icon="CirclePlus" :underline="false" @click.stop="dialogFromCli('addDeptDialogVisible', data, true)"></el-link>
-                      <el-link type="primary" :icon="Delete" :underline="false" style="margin-left: 6px;" @click.stop="deteleDept(data)"></el-link>
+                      <el-link type="primary" :icon="CirclePlus" :underline="false"
+                        @click.stop="dialogFromCli('addDeptDialogVisible', data, true)"></el-link>
+                      <el-link type="primary" :icon="Delete" :underline="false" style="margin-left: 6px;"
+                        @click.stop="deteleDept(data)"></el-link>
                     </div>
                   </div>
                 </div>
@@ -88,14 +107,26 @@
               <el-table-column type="selection" width="55" />
               <el-table-column label="姓名" property="name" width="150">
                 <template #default="scope">
-                  <TextTranslation translationTypes="userName" :translationValue="scope.row.name"></TextTranslation>
+                  <div class="flex items-center">
+                    <TextTranslation translationTypes="userName" :translationValue="scope.row.name"></TextTranslation>
+                    <template v-if="officialAccountInformation.id && scope.row.wxImgUrlWithTicket">
+                      <el-tooltip class="box-item" effect="dark" content="点击查看销售二维码" placement="top">
+                        <div class="ml-2 cursor-pointer" @click="viewQrCode(scope.row)">
+                          <el-icon color="#075985">
+                            <PictureFilled />
+                          </el-icon>
+                        </div>
+                      </el-tooltip>
+                    </template>
+                  </div>
                 </template>
               </el-table-column>
               <el-table-column label="手机" property="phone"></el-table-column>
               <el-table-column label="工号" property="jobNumber"></el-table-column>
               <el-table-column label="部门" property="departmentName">
                 <template #default="scope">
-                  <TextTranslation translationTypes="departmentName" :translationValue="scope.row.departmentName"></TextTranslation>
+                  <TextTranslation translationTypes="departmentName" :translationValue="scope.row.departmentName">
+                  </TextTranslation>
                 </template>
               </el-table-column>
               <el-table-column label="角色" property="roleName"></el-table-column>
@@ -159,7 +190,8 @@
         <el-form ref="deptRuleFormRef" style="max-width: 500px" :model="deptForm" :rules="deptRules" label-width="140px"
           size="large" status-icon>
           <el-form-item label="部门名称" prop="name">
-            <el-input v-model="deptForm.name" placeholder="请输入部门名称" clearable :disabled="userInfo.userNameNeedTranslate == 1" />
+            <el-input v-model="deptForm.name" placeholder="请输入部门名称" clearable
+              :disabled="userInfo.userNameNeedTranslate == 1" />
           </el-form-item>
           <el-form-item label="主要负责人">
             <!-- <el-select v-model="deptForm.managerId" placeholder="请选择" style="width: 100%" clearable>
@@ -171,7 +203,8 @@
             <!-- <el-select v-model="deptForm.otherManagerIds" placeholder="请选择" style="width: 100%" multiple clearable>
               <el-option v-for="item in userList" :key="item.id" :label="item.name" :value="item.id" />
             </el-select> -->
-            <personnel-search v-model="deptForm.otherManagerIds" :size="''" multiple placeholder="请选择"></personnel-search>
+            <personnel-search v-model="deptForm.otherManagerIds" :size="''" multiple
+              placeholder="请选择"></personnel-search>
           </el-form-item>
         </el-form>
       </div>
@@ -247,13 +280,15 @@
     </el-dialog>
 
     <!-- 同步企业微信通讯录 -->
-    <el-dialog v-model="dialogFrom.newSyncWithCorpWxDayloadVisable" width="600" :show-close="false" :before-close="handleClose">
+    <el-dialog v-model="dialogFrom.newSyncWithCorpWxDayloadVisable" width="600" :show-close="false"
+      :before-close="handleClose">
       <template #header="{ close, titleId, titleClass }">
         <div class="flex justify-between items-center border-b pb-3 dialog-header">
           <h4 :id="titleId">同步企微通讯录</h4>
           <div class="flex">
             <el-button @click="dialogFrom.newSyncWithCorpWxDayloadVisable = false">取消</el-button>
-            <el-button type="primary" :loading="loadingFrom.newSyncWithCorpWxDayloadLoading" @click="newSyncWithCorpWx()">
+            <el-button type="primary" :loading="loadingFrom.newSyncWithCorpWxDayloadLoading"
+              @click="newSyncWithCorpWx()">
               开始同步
             </el-button>
           </div>
@@ -264,6 +299,38 @@
       </div>
     </el-dialog>
 
+    <!-- 公众号配置 -->
+    <el-dialog v-model="dialogFrom.officialAccountSettingVisable" width="600" :show-close="false"
+      :before-close="handleClose" :close="closeOfficialAccountInformation(officialAccountInformationRef)">
+      <template #header="{ close, titleId, titleClass }">
+        <div class="flex justify-between items-center border-b pb-3 dialog-header">
+          <h4 :id="titleId">公众号配置</h4>
+          <div class="flex">
+            <el-button @click="dialogFrom.officialAccountSettingVisable = false">取消</el-button>
+            <el-button type="primary" :loading="loadingFrom.officialAccountSettingLoading"
+              @click="configureOfficialAccountInformation(officialAccountInformationRef)">
+              配置
+            </el-button>
+          </div>
+        </div>
+      </template>
+      <div class="pt-4 px-12 py-2">
+        <el-form ref="officialAccountInformationRef" style="max-width: 600px" :model="officialAccountInformationFrom"
+          status-icon :rules="officialAccountInformationFromRules" label-width="auto" class="demo-ruleForm">
+          <el-form-item label="名称:" prop="companyName">
+            <el-input v-model.trim="officialAccountInformationFrom.companyName" placeholder="请输入" />
+          </el-form-item>
+          <el-form-item label="appId:" prop="appId">
+            <el-input v-model.trim="officialAccountInformationFrom.appId" placeholder="请输入" />
+          </el-form-item>
+          <el-form-item label="appSecret:" prop="appSecret">
+            <el-input v-model.trim="officialAccountInformationFrom.appSecret" placeholder="请输入" />
+          </el-form-item>
+        </el-form>
+        <el-text class="mx-1" type="warning">请联系客服, 获取ip白名单参数, 配置后方可生效</el-text>
+      </div>
+    </el-dialog>
+
     <!-- 新增人员 -->
     <AddPersonnelModal :data="{
       addPersonnelDialogVisible: dialogFrom.addPersonnelDialogVisible,
@@ -275,17 +342,26 @@
     <!-- 批量操作 -->
     <BatchOperation :batchData="visibleData" :batchNode="batchTableData" :visibleText="allText.batchText"
       :popup="visibleType" :batchOperationVisible="dialogFrom.batchOperationVisible" @close="closeModal" />
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="showPreview"
+      :url-list="previewSrcList"
+      show-progress
+      :initial-index="0"
+      @close="showPreview = false"
+    />
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, reactive, onMounted, onBeforeMount, inject } from 'vue';
-import { UploadRequestOptions, dayjs, ElLoading } from 'element-plus'
-import { Search, CirclePlusFilled, Edit, CirclePlus, Delete } from '@element-plus/icons-vue'
+import { UploadRequestOptions, dayjs, ElLoading, ElNotification } from 'element-plus'
+import { Search, CirclePlusFilled, Edit, CirclePlus, Delete, PictureFilled } from '@element-plus/icons-vue'
 import { FormInstance, FormRules, ElMessageBox } from 'element-plus'
 import { useStore } from '@/store/index'
-import { GET_DATA_LIST, DETELE_DEPT, MOD, GET_USERINFO, GET_ROUTELIST, DEACTIVEUSER, SETACTIVE, GET_DEPTLIST, BACTHSERROLE, GET_USERLIST, GET_ADDDEPT, ADD_USER, SETRESETPWD, EXPOERTUSER, EDIT_ADDDEPT } from './api'
-import { post, uploadFile } from "@/utils/request";
+import { GET_DATA_LIST, DETELE_DEPT, MOD, GET_USERINFO, GET_ROUTELIST, DEACTIVEUSER, SETACTIVE, GET_DEPTLIST, BACTHSERROLE, GET_USERLIST, GET_ADDDEPT, ADD_USER, SETRESETPWD, EXPOERTUSER, EDIT_ADDDEPT, GET_COMPANY_WEI_XIN, SAVE_OR_UPDATE, ONE_CLICK_GENERATION, EXPORT_QR_CODE } from './api'
+import { post, uploadFile, downloadFileRequest } from "@/utils/request";
 import { getFromValue, updateDepTreeData, resetFromValue, confirmAction, downloadFile } from '@/utils/tools'
 import { storeToRefs } from 'pinia';
 
@@ -310,6 +386,13 @@ interface deptRuleForm { // 部门表单类型
   otherManagerIds: string[] | number[],
 }
 
+interface officialAccountInformationInterface { // 公众号类型
+  id?: string | number,
+  companyName: string,
+  appId: string,
+  appSecret: string,
+}
+
 // 固定数据
 const stateOptions = [{ value: '3', label: '全部' }, { value: '1', label: '在职' }, { value: '0', label: '停用' }]
 
@@ -325,7 +408,8 @@ const loadingFrom = reactive({ // 所有加载状态
   resignationLoading: false,
   exportUserLoading: false,
   importLoading: false,
-  newSyncWithCorpWxDayloadLoading: false
+  newSyncWithCorpWxDayloadLoading: false,
+  officialAccountSettingLoading: false,
 })
 const dialogFrom: any = reactive({ // 所有弹窗状态
   addDeptDialogVisible: false,
@@ -334,7 +418,8 @@ const dialogFrom: any = reactive({ // 所有弹窗状态
   resignationVisible: false,
   exportUserVisible: false,
   importVisible: false,
-  newSyncWithCorpWxDayloadVisable: false
+  newSyncWithCorpWxDayloadVisable: false,
+  officialAccountSettingVisable: false,
 });
 const allText = reactive({
   batchText: '批量操作'
@@ -375,12 +460,61 @@ const treeProps = { // 部门树配置
   children: 'children',
   label: 'label',
 }
+const officialAccountInformation = ref<officialAccountInformationInterface>({
+  id: '',
+  companyName: '',
+  appId: '',
+  appSecret: '',
+})
+
+const officialAccountInformationFrom = ref<officialAccountInformationInterface>({
+  id: '',
+  companyName: '',
+  appId: '',
+  appSecret: '',
+})
+const officialAccountInformationRef = ref<FormInstance>()
+const showPreview = ref(false)
+const previewSrcList = ref<string[]>([])
 
 // 定义校验规则
 const deptRules = reactive<FormRules<typeof deptForm>>({ // 部门表单校验规则
   name: [{ required: true, trigger: 'blur', message: '请输入部门名称' }]
 })
 
+const officialAccountInformationFromRules = reactive<FormRules<typeof officialAccountInformation>>({
+  companyName: [{ required: true, trigger: 'blur', message: '请输入' }],
+  appId: [{ required: true, trigger: 'blur', message: '请输入' }],
+  appSecret: [{ required: true, trigger: 'blur', message: '请输入' }]
+})
+
+function viewQrCode(row: any) {
+  const { wxImgUrlWithTicket = '' } = row
+  previewSrcList.value = [wxImgUrlWithTicket]
+  showPreview.value = true
+}
+
+function exportQrCode() {
+  ElNotification({
+    title: '提示',
+    message: '二维码导出中...',
+    type: 'info',
+    showClose: false,
+    duration: 0
+  })
+  downloadFileRequest(EXPORT_QR_CODE, {}).then((blob) => {
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.setAttribute('download', '员工销售二维码.zip'); // 根据需要设定文件名
+    document.body.appendChild(link);
+    link.click();
+    link.remove();
+    window.URL.revokeObjectURL(url);
+    globalPopup?.showSuccess('下载中 请稍后')
+  })
+}
+
 // 同步企业微信通讯录
 function newSyncWithCorpWx() {
   const loading = ElLoading.service({
@@ -419,6 +553,58 @@ async function importUser(param: UploadRequestOptions) {
   globalPopup?.showError(res.msg || '')
 }
 
+// 获取公众号配置信息
+function getCompanyWeiXin() {
+  post(GET_COMPANY_WEI_XIN, {}).then((res) => {
+    const { companyName = '', id = '', appId = '', appSecret = '' } = res.data
+    officialAccountInformation.value = {
+      id, companyName, appId, appSecret
+    }
+  })
+}
+
+function oneClickGenerationOfQrCode() {
+  ElNotification({
+    title: '提示',
+    message: '生成二维码中...',
+    type: 'info',
+    showClose: false,
+    duration: 0
+  })
+  post(ONE_CLICK_GENERATION, {}).then(() => {
+    globalPopup?.showSuccess('生成成功')
+    getTableData()
+  })
+}
+
+async function configureOfficialAccountInformation(formEl: FormInstance | undefined) {
+  if (!formEl) return
+  await formEl.validate((valid) => {
+    if (valid) {
+      confirmAction('确定保存当前公众号的配置吗?', '公众号配置', 'warning').then(() => {
+        loadingFrom.officialAccountSettingLoading = true
+        post(SAVE_OR_UPDATE, getFromValue(officialAccountInformationFrom.value)).then(() => {
+          globalPopup?.showSuccess('配置成功')
+          closeModal('officialAccountSettingVisable')
+          getCompanyWeiXin()
+        }).finally(() => {
+          loadingFrom.officialAccountSettingLoading = false
+        })
+      })
+    }
+  })
+}
+
+function closeOfficialAccountInformation(formEl: FormInstance | undefined) {
+  if (!formEl) return
+  formEl.resetFields()
+}
+
+function officialAccountSetting() {
+  officialAccountInformationFrom.value = JSON.parse(JSON.stringify(officialAccountInformation.value))
+  dialogFrom.officialAccountSettingVisable = true
+}
+
 function exportUser() {
   loadingFrom.exportUserLoading = true
   post(EXPOERTUSER, { containInvalid: exportRadio.value }).then((res) => {
@@ -490,7 +676,7 @@ function addPersone(item: any) {
   post(GET_USERINFO, { userId: item.id }).then(res => {
     const { id, name, phone, jobNumber, roleId, departmentCascade, departmentId, inductionDate } = res.data
     let newData = {
-      id, name, phone, jobNumber, roleId, 
+      id, name, phone, jobNumber, roleId,
       // departmentId: departmentCascade && departmentCascade.split(',').map(Number).reverse(),
       departmentId: departmentId,
       inductionDate
@@ -511,7 +697,7 @@ async function personnelModalConfirm(data: any, modelType: string) {
     globalPopup?.showSuccess('添加成功')
     getTableData()
   }).catch(_err => {
-    dialogFrom[modelType] = false 
+    dialogFrom[modelType] = false
   })
 }
 
@@ -707,6 +893,7 @@ onMounted(() => {
   getUserList()
   getTableData()
   getDeptList()
+  getCompanyWeiXin()
 });
 </script>
 
@@ -751,6 +938,7 @@ onMounted(() => {
       align-items: center;
       justify-self: flex-end;
     }
+
     .nodeEle {
       display: none;
     }
@@ -763,7 +951,7 @@ onMounted(() => {
 </style>
 
 <style>
-  .temaClass .el-tree-node {
-    padding: 4px 0;
-  }
+.temaClass .el-tree-node {
+  padding: 4px 0;
+}
 </style>

+ 53 - 5
fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/request.ts

@@ -30,17 +30,20 @@ instance.interceptors.request.use(
 instance.interceptors.response.use(
   (response: AxiosResponse) => {
     // 对响应数据进行处理
-    if (response.status !== 200 || response.data.code === 'error') {
-      ElNotification.closeAll()
+    if (response.status !== 200 || response.data.code === "error") {
+      ElNotification.closeAll();
       ElNotification({
-        message: response.status !== 200 ? showMessage(response.status) : response.data.msg,
+        message:
+          response.status !== 200
+            ? showMessage(response.status)
+            : response.data.msg,
         type: "error",
       });
     }
     return response;
   },
   (error: AxiosError) => {
-    ElNotification.closeAll()
+    ElNotification.closeAll();
     ElNotification({
       message: showMessage(error.request.status), // 传入响应码,匹配响应码对应信息,
       type: "error",
@@ -51,7 +54,11 @@ instance.interceptors.response.use(
 );
 
 // 封装GET请求
-export async function get(url: string, params?: any, file: boolean = false): Promise<any> {
+export async function get(
+  url: string,
+  params?: any,
+  file: boolean = false
+): Promise<any> {
   return new Promise((resolve, reject) => {
     instance
       .get(url, { params })
@@ -114,3 +121,44 @@ export async function uploadFile(url: string, data?: any): Promise<any> {
       });
   });
 }
+
+// 封装文件流请求(下载)
+export async function downloadFileRequest(url: string, params?: any): Promise<Blob> {
+  return new Promise((resolve, reject) => {
+    instance
+      .get(url, {
+        params,
+        responseType: "blob", // 设置响应类型为 blob 以处理文件流
+      })
+      .then((response: AxiosResponse<Blob>) => {
+        // 判断 blob 是否为错误信息(如后端返回错误内容也可能是 JSON)
+        const contentType = response.headers["content-type"];
+        if (contentType && contentType.includes("application/json")) {
+          const reader = new FileReader();
+          reader.onload = () => {
+            const result = reader.result as string;
+            try {
+              const json = JSON.parse(result);
+              ElNotification({
+                message: json.msg || "下载失败",
+                type: "error",
+              });
+              reject(json);
+            } catch (e) {
+              resolve(response.data);
+            }
+          };
+          reader.readAsText(response.data);
+        } else {
+          resolve(response.data);
+        }
+      })
+      .catch((error) => {
+        ElNotification({
+          message: showMessage(error.request.status),
+          type: "error",
+        });
+        reject(error);
+      });
+  });
+}

+ 5 - 0
fhKeeper/formulahousekeeper/management-crm/pom.xml

@@ -21,6 +21,11 @@
             <scope>system</scope>
             <systemPath>${basedir}/src/main/resources/lib/taobao-sdk-java-auto_1479188381469-20210623.jar</systemPath>
         </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.11.0</version>
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>

+ 21 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/UserQrCodeController.java

@@ -0,0 +1,21 @@
+package com.management.platform.controller;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ *  前端控制器
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-16
+ */
+@RestController
+@RequestMapping("/user-qr-code")
+public class UserQrCodeController {
+
+}
+

+ 420 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WechatAccountController.java

@@ -0,0 +1,420 @@
+package com.management.platform.controller;
+
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.management.platform.entity.User;
+import com.management.platform.entity.UserQrCode;
+import com.management.platform.entity.WechatAccount;
+import com.management.platform.service.CompanyService;
+import com.management.platform.service.UserQrCodeService;
+import com.management.platform.service.UserService;
+import com.management.platform.service.WechatAccountService;
+import com.management.platform.util.FileZipUtil;
+import com.management.platform.util.HttpRespMsg;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContexts;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 微信公众号账号表 前端控制器
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-15
+ */
+@Slf4j
+@RestController
+@RequestMapping("/wechat-account")
+public class WechatAccountController {
+
+    @Resource
+    private WechatAccountService wechatAccountService;
+
+    @Resource
+    private UserService userService;
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    @Resource
+    private UserQrCodeService userQrCodeService;
+
+    @Resource
+    private CompanyService companyService;
+
+    @Value(value ="${upload.path}")
+    private String uploadPath;
+
+    private final static String prefixUrl="https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
+
+
+    //保存或修改
+    @RequestMapping("/saveOrUpdate")
+    public HttpRespMsg saveOrUpdate(WechatAccount wechatAccount ,HttpServletRequest request) {
+        HttpRespMsg httpRespMsg = new HttpRespMsg();
+        String token = request.getHeader("Token");
+        User user = userService.getById(token);
+        if (wechatAccount.getId()==null){
+            int count = wechatAccountService.count(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+            if (count>0){
+                httpRespMsg.setError("公司配置已存在,请传公司关键信息");
+                return httpRespMsg;
+            }
+            wechatAccount.setCompanyId(user.getCompanyId());
+            wechatAccountService.save(wechatAccount);
+        }else {
+            wechatAccountService.update(null,new UpdateWrapper<WechatAccount>()
+                    .set("company_name", wechatAccount.getCompanyName())
+                    .set("app_id", wechatAccount.getAppId())
+                    .set("app_secret", wechatAccount.getAppSecret())
+                    .eq("id", wechatAccount.getId()));
+
+        }
+        return httpRespMsg;
+    }
+
+    //获取企业微信公众号配置
+    @RequestMapping("/getCompanyWeiXin")
+    public HttpRespMsg getCompanyWeiXin(HttpServletRequest request) {
+        HttpRespMsg httpRespMsg = new HttpRespMsg();
+        String token = request.getHeader("Token");
+        User user = userService.getById(token);
+        WechatAccount wechatAccount = wechatAccountService.getOne(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+        httpRespMsg.setData(wechatAccount);
+        return httpRespMsg;
+    }
+
+
+    /**
+     * 保存修改并获取公司对应的accessToken
+     * @param request
+     * @return
+     */
+    @RequestMapping("/getAccessToken")
+    public HttpRespMsg getAccessToken(HttpServletRequest request) {
+        HttpRespMsg httpRespMsg = new HttpRespMsg();
+        String token = request.getHeader("Token");
+        User user = userService.getById(token);
+        WechatAccount wechatAccount = wechatAccountService.getOne(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+        if (wechatAccount==null){
+            httpRespMsg.setError("该公司没有配置公众号相关的参数");
+            return httpRespMsg;
+        }
+        String accessToken = wechatAccountService.getAccessToken(user.getCompanyId(), wechatAccount.getAppId());
+        httpRespMsg.setData(accessToken);
+        return httpRespMsg;
+    }
+
+    @RequestMapping(value = "sendInterviewInvitation")
+    public HttpRespMsg sendInterviewInvitation(String userId,HttpServletRequest request) {
+        HttpRespMsg msg = new HttpRespMsg();
+        String token = request.getHeader("Token");
+        User user = userService.getById(token);
+        WechatAccount wechatAccount = wechatAccountService.getOne(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+        String accessToken = wechatAccountService.getAccessToken(user.getCompanyId(), wechatAccount.getAppId());
+        try {
+
+            String result =createForeverStrTicket(accessToken, userId);
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            String ticket = null ;
+            if(null!=jsonObject){
+                ticket = jsonObject.getString("ticket");
+                System.out.println("创建永久带参二维码成功,ticket="+ticket);
+            }
+            msg.data=ticket;
+            return msg;
+        } catch (Exception e) {
+            msg.setData(e.getMessage());
+        }
+        return msg;
+    }
+
+
+    //根据用户id获取对应的二维码信息
+    @RequestMapping(value = "getQrCodeFromTableOrWX")
+    public HttpRespMsg getQrCodeFromTableOrWX(String userId,HttpServletRequest request) {
+        HttpRespMsg msg = new HttpRespMsg();
+        String token = request.getHeader("Token");
+        User user = userService.getById(token);
+        UserQrCode qrCode = userQrCodeService.getOne(new QueryWrapper<UserQrCode>().eq("user_id", userId));
+        if (qrCode!=null){
+            qrCode.setUrlPlusTicket(prefixUrl+qrCode.getTicket());
+            msg.data=qrCode;
+            return msg;
+        }
+        else {
+            WechatAccount wechatAccount = wechatAccountService.getOne(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+            if (wechatAccount==null){
+                msg.setError("该公司没有配置公众号相关的参数");
+                return msg;
+            }
+            //获取公众号token
+            String accessToken = wechatAccountService.getAccessToken(user.getCompanyId(), wechatAccount.getAppId());
+            try {
+                //获取二维码相关信息相关
+                String result =createForeverStrTicket(accessToken, userId);
+                if (result==null|| StringUtils.isEmpty(result)){
+                    msg.setError("获取二维码信息失败");
+                    return msg;
+                }else {
+                    JSONObject jsonObject = JSONObject.parseObject(result);
+
+                    if (null != jsonObject) {
+                        String ticket = jsonObject.getString("ticket");
+                        System.out.println("创建永久带参二维码成功,ticket=" + ticket);
+                        String url = jsonObject.getString("url");
+                        System.out.println("创建永久带参二维码成功,url=" + url);
+
+                        UserQrCode userQrCode = new UserQrCode();
+                        userQrCode.setUserId(userId);
+                        userQrCode.setTicket(ticket);
+                        userQrCode.setUrl(url);
+                        userQrCode.setCompanyId(user.getCompanyId());
+                        //保存图片
+                        String imgPrefix=(StringUtils.isEmpty(user.getJobNumber())?"":user.getJobNumber())+"_"+user.getName();
+                        CompletableFuture<String> future = saveWeChatQrCodeImgHttpClient(prefixUrl + ticket, imgPrefix, uploadPath);
+                        String imgName = future.get(10, TimeUnit.SECONDS);// 添加超时
+                        if (imgName.isEmpty()){
+                            msg.setError("二维码图片保存失败");
+                            return msg;
+                        }else {
+                            userQrCode.setImg(imgName);
+                        }
+
+                        userQrCodeService.save(userQrCode);
+                        userQrCode.setUrlPlusTicket(prefixUrl+userQrCode.getTicket());
+                        msg.setData(userQrCode);
+                    }
+
+                    return msg;
+                }
+            } catch (Exception e) {
+                msg.setData(e.getMessage());
+            }
+            return msg;
+        }
+    }
+
+    //一键生成所有员工的二维码
+    @RequestMapping(value = "oneClickGeneration")
+    public HttpRespMsg oneClickGeneration(HttpServletRequest request) {
+        HttpRespMsg msg = new HttpRespMsg();
+        String token = request.getHeader("Token");
+        User user = userService.getById(token);
+
+        WechatAccount wechatAccount = wechatAccountService.getOne(new QueryWrapper<WechatAccount>().eq("company_id", user.getCompanyId()));
+        if (wechatAccount==null){
+            msg.setError("该公司没有配置公众号相关的参数");
+            return msg;
+        }
+        //获取公众号token
+        String accessToken = wechatAccountService.getAccessToken(user.getCompanyId(), wechatAccount.getAppId());
+
+        List<UserQrCode> qrCodeList = userQrCodeService.list(new QueryWrapper<UserQrCode>().eq("company_id", user.getCompanyId()));
+        List<String> userAlreadyIds = qrCodeList.stream().map(UserQrCode::getUserId).collect(Collectors.toList());//已经生成二维码的员工
+        userAlreadyIds.add("-1");
+
+        List<User> userList = userService.list(new QueryWrapper<User>().notIn("id", userAlreadyIds)
+                .eq("company_id", user.getCompanyId()));//未生成二维码的员工
+        if (!userList.isEmpty()){
+            for (User u : userList) {
+                String userId = u.getId();
+                try {
+                    //获取二维码相关信息相关
+                    String result =createForeverStrTicket(accessToken, userId);
+                    if (result==null|| StringUtils.isEmpty(result)){
+                        msg.setError("获取二维码信息失败");
+                        return msg;
+                    }else {
+                        JSONObject jsonObject = JSONObject.parseObject(result);
+                        if (null != jsonObject) {
+                            String ticket = jsonObject.getString("ticket");
+                            System.out.println("创建永久带参二维码成功,ticket=" + ticket);
+                            String url = jsonObject.getString("url");
+                            System.out.println("创建永久带参二维码成功,url=" + url);
+
+                            UserQrCode userQrCode = new UserQrCode();
+                            userQrCode.setUserId(userId);
+                            userQrCode.setTicket(ticket);
+                            userQrCode.setUrl(url);
+                            userQrCode.setCompanyId(user.getCompanyId());
+                            //保存图片
+                            String imgPrefix=(StringUtils.isEmpty(u.getJobNumber())?"":u.getJobNumber())+"_"+u.getName();
+                            CompletableFuture<String> future = saveWeChatQrCodeImgHttpClient(prefixUrl + ticket, imgPrefix, uploadPath);
+                            String imgName = future.get(10, TimeUnit.SECONDS);// 添加超时
+                            if (imgName.isEmpty()){
+                                msg.setError("二维码图片保存失败");
+                                return msg;
+                            }else {
+                                userQrCode.setImg(imgName);
+                            }
+                            userQrCodeService.save(userQrCode);
+                        }
+                    }
+                } catch (Exception e) {
+                    msg.setData(e.getMessage());
+                }
+            }
+        }
+        return msg;
+    }
+
+
+
+
+    @Async("taskExecutor")
+    public CompletableFuture<String> saveWeChatQrCodeImg(String qrCodeUrl, String ticket, String uploadPath) {
+        try {
+            log.info("开始保存二维码图片,ticket: {}", ticket);
+
+            // 确保目录存在
+            File uploadDir = new File(uploadPath);
+            if (!uploadDir.exists() && !uploadDir.mkdirs()) {
+                throw new IOException("无法创建目录: " + uploadPath);
+            }
+
+            // 构建目标文件路径
+            String fileName = ticket + ".jpg";
+            File destFile = new File(uploadPath, fileName);
+
+            // 从URL下载图片
+            log.info("qrCodeUrl==>{}", qrCodeUrl);
+            ResponseEntity<byte[]> response = restTemplate.getForEntity(qrCodeUrl, byte[].class);
+
+
+            if (!response.getStatusCode().is2xxSuccessful()) {
+                throw new IOException("下载失败,HTTP 状态码: " + response.getStatusCodeValue());
+            }
+
+            // 写入文件
+            try (FileOutputStream fos = new FileOutputStream(destFile)) {
+                fos.write(response.getBody());
+            }
+
+            log.info("二维码图片保存成功,文件名: {}", fileName);
+            return CompletableFuture.completedFuture(fileName);
+
+        } catch (IOException e) {
+            log.error("保存二维码图片失败,ticket: {}", ticket, e);
+            return CompletableFuture.completedFuture("");
+        }
+    }
+
+    @Async("taskExecutor")
+    public CompletableFuture<String> saveWeChatQrCodeImgHttpClient(String qrCodeUrl, String imgPrefix, String uploadPath) {
+        try {
+
+
+            // 确保目录存在
+            File uploadDir = new File(uploadPath);
+            if (!uploadDir.exists() && !uploadDir.mkdirs()) {
+                throw new IOException("无法创建目录: " + uploadPath);
+            }
+
+            // 构建目标文件路径
+            String fileName = imgPrefix + ".jpg";
+            File destFile = new File(uploadPath, fileName);
+
+
+            log.info("qrCodeUrl==>{}", qrCodeUrl);
+            // 创建信任所有证书的HttpClient
+            HttpClient client = HttpClients.custom()
+                    .setSSLContext(SSLContexts.custom().loadTrustMaterial(null, (chain, authType) -> true).build())
+                    .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
+                    .build();
+
+            HttpGet request = new HttpGet(qrCodeUrl);
+            try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(request);
+                 FileOutputStream fos = new FileOutputStream(destFile)) {
+                if (response.getStatusLine().getStatusCode() != 200) {
+                    throw new IOException("下载失败: " + response.getStatusLine());
+                }
+                response.getEntity().writeTo(fos);
+            }
+            return CompletableFuture.completedFuture(fileName);
+        } catch (Exception e) {
+            return CompletableFuture.completedFuture("");
+        }
+    }
+
+
+    /**
+     * 创建永久二维码(字符串)
+     *
+     * @param accessToken
+     * @param sceneStr
+     *            场景str
+     * @return
+     */
+    public String createForeverStrTicket(String accessToken, String sceneStr){
+        Map<String, String> intMap = new HashMap<>();
+        intMap.put("scene_str", sceneStr);
+        Map<String, Map<String, String>> mapMap = new HashMap<>();
+        mapMap.put("scene", intMap);
+        Map<String, Object> paramsMap = new HashMap<>();
+        paramsMap.put("action_name", "QR_LIMIT_STR_SCENE");
+        paramsMap.put("action_info", mapMap);
+        String  jsonObject = JSONObject.toJSONString(paramsMap);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        String requestUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token="+accessToken;
+
+
+        HttpEntity<String> requestEntity = new HttpEntity<String>(jsonObject, headers);
+        ResponseEntity<String> responseEntity = restTemplate.exchange(requestUrl,
+                HttpMethod.POST, requestEntity, String.class);
+
+        if (responseEntity.getStatusCode() == HttpStatus.OK) {
+            JSONObject json = JSONObject.parseObject(responseEntity.getBody());
+            assert json != null;
+            log.info("json.toJSONString()"+json.toJSONString());
+            return json.toJSONString();
+        }
+        else return "";
+    }
+
+    /**
+     * 导出
+     */
+    @RequestMapping("/export")
+    public void getExport(HttpServletRequest request, HttpServletResponse response)  {
+        String token = request.getHeader("TOKEN");
+        User user = userService.getById(token);
+        List<UserQrCode> codeList = userQrCodeService.list(new QueryWrapper<UserQrCode>().eq("company_id", user.getCompanyId()));
+
+        List<String> fileNames= codeList.stream().map(UserQrCode::getImg).collect(Collectors.toList());
+        if (!fileNames.isEmpty()){
+            FileZipUtil.exportZip(response,uploadPath ,fileNames, "员工微信公众号二维码压缩包", ".zip");
+        }else return;
+    }
+
+}
+

+ 182 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/WeiXinTicketController.java

@@ -0,0 +1,182 @@
+package com.management.platform.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.management.platform.entity.WeiXinTicketEntity;
+import com.management.platform.service.WechatAccountService;
+import com.management.platform.util.HttpRespMsg;
+import com.management.platform.util.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 生成带参二维码的ticket值
+ * @author wyx
+ *
+ */
+
+@RestController
+@RequestMapping("weixinTicket")
+
+@Slf4j
+public class WeiXinTicketController {
+
+	@Value("${weixin.ticket.action_name.QR_SCENE}")
+    private String 	QRSCENE;
+	@Value("${weixin.ticket.action_name.QR_STR_SCENE}")
+    private String 	QR_STR_SCENE;
+
+	private static final String APP_ID = "wx1c1d8fc81bc073a8";
+	private static final String APP_SECRET = "17ad07f90ee845f99f4c1605647ef755";
+	private static final String GRANT_TYPE = "client_credential";
+
+	@Resource
+	private  RestTemplate restTemplate;
+
+	@Resource
+	private WechatAccountService wechatAccountService;
+
+
+	
+	@RequestMapping(value = "sendInterviewInvitation",method=RequestMethod.POST)
+	public HttpRespMsg sendInterviewInvitation(@RequestBody WeiXinTicketEntity reqDto) throws Exception{
+		HttpRespMsg msg = new HttpRespMsg();
+		try {
+			String accessToken = wechatAccountService.getAccessToken(6374,"");
+			String sceneStr = reqDto.getSceneStr();
+			String result =this.createForeverStrTicket(accessToken, sceneStr);
+			JSONObject jsonObject = JSONObject.parseObject(result);
+			String ticket = null ;
+			if(null!=jsonObject){
+				ticket = jsonObject.getString("ticket");  
+                System.out.println("创建永久带参二维码成功,ticket="+ticket);  
+	        }    
+			msg.data=ticket;
+			return msg;
+		} catch (Exception e) {
+			throw new Exception(e);
+		}
+	}
+	
+	/**
+	 * 创建临时带参数二维码 
+	 * 获取灵石带参二维码ticket
+	 * 
+	 * @param accessToken
+	 * @expireSeconds 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为30秒。
+	 * @param sceneId 场景Id
+	 * @return
+	 */
+	public String createTempTicket(String accessToken, String expireSeconds, int sceneId) throws Exception {
+		Map<String, Integer> intMap = new HashMap<>();
+		intMap.put("scene_id", sceneId);
+		Map<String, Map<String, Integer>> mapMap = new HashMap<>();
+		mapMap.put("scene", intMap);
+		Map<String, Object> paramsMap = new HashMap<>();
+		paramsMap.put("expire_seconds", expireSeconds);
+		paramsMap.put("action_name", QRSCENE);
+		paramsMap.put("action_info", mapMap);
+		String  jsonObject = JSONObject.toJSONString(paramsMap);
+		String requestUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token="+accessToken;
+		String result = HttpUtil.post(requestUrl,accessToken, jsonObject);
+		return result;
+	 
+	}
+	 
+	/**
+	 * 创建永久二维码(数字)
+	 * 
+	 * @param accessToken
+	 * @param sceneId
+	 *            场景Id
+	 * @return
+	 */
+	public String createForeverTicket(String accessToken, int sceneId) throws Exception {
+		Map<String, Integer> intMap = new HashMap<>();
+		intMap.put("scene_id", sceneId);
+		Map<String, Map<String, Integer>> mapMap = new HashMap<>();
+		mapMap.put("scene", intMap);
+		Map<String, Object> paramsMap = new HashMap<>();
+		paramsMap.put("action_name", "QR_LIMIT_STR_SCENE");
+		paramsMap.put("action_info", mapMap);
+		String  jsonObject = JSONObject.toJSONString(paramsMap);
+		String requestUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=";
+		String result = HttpUtil.post(requestUrl,accessToken, jsonObject);
+		return result;
+	}
+	 
+	/**
+	 * 创建永久二维码(字符串)
+	 * 
+	 * @param accessToken
+	 * @param sceneStr
+	 *            场景str
+	 * @return
+	 */
+	public String createForeverStrTicket(String accessToken, String sceneStr){
+		Map<String, String> intMap = new HashMap<>();
+		intMap.put("scene_str", sceneStr);
+		Map<String, Map<String, String>> mapMap = new HashMap<>();
+		mapMap.put("scene", intMap);
+		Map<String, Object> paramsMap = new HashMap<>();
+		paramsMap.put("action_name", "QR_LIMIT_STR_SCENE");
+		paramsMap.put("action_info", mapMap);
+		String  jsonObject = JSONObject.toJSONString(paramsMap);
+
+		HttpHeaders headers = new HttpHeaders();
+		headers.setContentType(MediaType.APPLICATION_JSON);
+		String requestUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token="+accessToken;
+
+
+		HttpEntity<String> requestEntity = new HttpEntity<String>(jsonObject, headers);
+		ResponseEntity<String> responseEntity = this.restTemplate.exchange(requestUrl,
+				HttpMethod.POST, requestEntity, String.class);
+
+		if (responseEntity.getStatusCode() == HttpStatus.OK) {
+			JSONObject json = JSONObject.parseObject(responseEntity.getBody());
+			log.info("返回:" + json.toJSONString());
+			if (json.getIntValue("errcode") == 0) {
+				log.info("");
+			}
+		}
+
+		return "";
+
+	}
+
+
+	//获取第三方应用临时凭证
+	private String getWeiXinAccessToken() {
+		RestTemplate restTemplate = new RestTemplate();
+		String response = restTemplate.getForObject(
+				"https://api.weixin.qq.com/cgi-bin/token",
+				String.class,
+				GRANT_TYPE,
+				APP_ID,
+				APP_SECRET);
+
+		JSONObject jsonObject = JSON.parseObject(response);
+		if (jsonObject.containsKey("access_token")) {
+			return jsonObject.getString("access_token");
+		} else {
+			System.err.println("获取access_token失败: " + response);
+			return null;
+		}
+
+	}
+
+
+
+ 
+ 
+}

+ 69 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/UserQrCode.java

@@ -0,0 +1,69 @@
+package com.management.platform.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+public class UserQrCode extends Model<UserQrCode> {
+
+    private static final long serialVersionUID=1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * useId
+     */
+    @TableField("user_id")
+    private String userId;
+
+    /**
+     * 公司id
+     */
+    @TableField("company_id")
+    private Integer companyId;
+
+    /**
+     * 根据员工userId获取的永久公众号ticket
+     */
+    @TableField("ticket")
+    private String ticket;
+
+    /**
+     * 根据员工userId获取的永久公众号url
+     */
+    @TableField("url")
+    private String url;
+
+    /**
+     * 二维码名称
+     */
+    @TableField("img")
+    private String img;
+
+    @TableField(exist = false)
+    private String urlPlusTicket;
+
+    @Override
+    protected Serializable pkVal() {
+        return this.id;
+    }
+
+}

+ 89 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WechatAccount.java

@@ -0,0 +1,89 @@
+package com.management.platform.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 微信公众号账号表
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-15
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+public class WechatAccount extends Model<WechatAccount> {
+
+    private static final long serialVersionUID=1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 企业ID
+     */
+    @TableField("company_id")
+    private Integer companyId;
+
+
+    /**
+     * 企业公众号名称
+     */
+    @TableField("company_name")
+    private String companyName;
+
+    /**
+     * 公众号AppID
+     */
+    @TableField("app_id")
+    private String appId;
+
+    /**
+     * 公众号AppSecret
+     */
+    @TableField("app_secret")
+    private String appSecret;
+
+    /**
+     * 访问令牌
+     */
+    @TableField("access_token")
+    private String accessToken;
+
+    /**
+     * 令牌过期时间
+     */
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("token_expire_time")
+    private LocalDateTime tokenExpireTime;
+
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("create_time")
+    private LocalDateTime createTime;
+
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+
+
+    @Override
+    protected Serializable pkVal() {
+        return this.id;
+    }
+
+}

+ 18 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/WeiXinTicketEntity.java

@@ -0,0 +1,18 @@
+package com.management.platform.entity;
+ 
+import lombok.Data;
+ 
+/**
+ * 
+ * @author 
+ *
+ */
+@Data
+public class WeiXinTicketEntity {
+	/*场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1--100000)*/
+	private int sceneId;
+	/*该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为30秒*/
+	private String expireSeconds;
+	/*场景值ID(字符串形式的ID),字符串类型,长度限制为1到64*/
+	private String sceneStr;
+}

+ 16 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/UserQrCodeMapper.java

@@ -0,0 +1,16 @@
+package com.management.platform.mapper;
+
+import com.management.platform.entity.UserQrCode;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-16
+ */
+public interface UserQrCodeMapper extends BaseMapper<UserQrCode> {
+
+}

+ 31 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/WechatAccountMapper.java

@@ -0,0 +1,31 @@
+package com.management.platform.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.management.platform.entity.WechatAccount;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 微信公众号账号表 Mapper 接口
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-15
+ */
+public interface WechatAccountMapper extends BaseMapper<WechatAccount> {
+    @Select("SELECT * FROM wechat_account WHERE company_id = #{companyId} AND app_id = #{appId}")
+    WechatAccount findByCompanyAndApp(@Param("companyId") Integer companyId,
+                                      @Param("appId") String appId);
+
+    @Update("UPDATE wechat_account SET access_token = #{accessToken}, " +
+            "token_expire_time = #{tokenExpireTime} " +
+            "WHERE company_id = #{companyId} AND app_id = #{appId}")
+    int updateTokenInfo(@Param("companyId") Integer companyId,
+                        @Param("appId") String appId,
+                        @Param("accessToken") String accessToken,
+                        @Param("tokenExpireTime") LocalDateTime tokenExpireTime);
+}

+ 16 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/UserQrCodeService.java

@@ -0,0 +1,16 @@
+package com.management.platform.service;
+
+import com.management.platform.entity.UserQrCode;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-16
+ */
+public interface UserQrCodeService extends IService<UserQrCode> {
+
+}

+ 16 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/WechatAccountService.java

@@ -0,0 +1,16 @@
+package com.management.platform.service;
+
+import com.management.platform.entity.WechatAccount;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 微信公众号账号表 服务类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-15
+ */
+public interface WechatAccountService extends IService<WechatAccount> {
+    public String getAccessToken(Integer companyId, String appId) ;
+}

+ 20 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/UserQrCodeServiceImpl.java

@@ -0,0 +1,20 @@
+package com.management.platform.service.impl;
+
+import com.management.platform.entity.UserQrCode;
+import com.management.platform.mapper.UserQrCodeMapper;
+import com.management.platform.service.UserQrCodeService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-16
+ */
+@Service
+public class UserQrCodeServiceImpl extends ServiceImpl<UserQrCodeMapper, UserQrCode> implements UserQrCodeService {
+
+}

+ 14 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/UserServiceImpl.java

@@ -193,6 +193,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
     private SysDictService sysDictService;
     @Resource
     private SuperSonicConfig  superSonicConfig;
+    @Resource
+    private UserQrCodeService userQrCodeService;
+
+    private final static String prefixUrl="https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
 
 
     //登录网页端
@@ -987,6 +991,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
                     }
                     list = active;
                 }
+                List<UserQrCode> qrCodeList = userQrCodeService.list(new QueryWrapper<UserQrCode>().eq("company_id", companyId));
+                for (Map<String, Object> map : list) {
+                    String userId = map.get("id").toString();
+                    Optional<UserQrCode> first = qrCodeList.stream().filter(q -> userId.equals(q.getUserId())).findFirst();
+                    if (first.isPresent()) {
+                        UserQrCode userQrCode = first.get();
+                        map.put("wxImgName",org.apache.commons.lang3.StringUtils.isEmpty(userQrCode.getImg())?null:userQrCode.getImg());
+                        map.put("wxImgUrlWithTicket", org.apache.commons.lang3.StringUtils.isEmpty(userQrCode.getTicket())?null:prefixUrl+userQrCode.getTicket());
+                    }
+                }
                 resultMap.put("records", list);
                 resultMap.put("total", total);
                 resultMap.put("nextCursor", "");

+ 99 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/WechatAccountServiceImpl.java

@@ -0,0 +1,99 @@
+package com.management.platform.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.management.platform.entity.WechatAccount;
+import com.management.platform.mapper.WechatAccountMapper;
+import com.management.platform.service.WechatAccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 微信公众号账号表 服务实现类
+ * </p>
+ *
+ * @author Seyason
+ * @since 2025-04-15
+ */
+@Service
+public class WechatAccountServiceImpl extends ServiceImpl<WechatAccountMapper, WechatAccount> implements WechatAccountService {
+
+    @Autowired
+    private  WechatAccountMapper wechatAccountMapper;
+    @Autowired
+    private  RestTemplate restTemplate;
+
+    // 提前5分钟刷新,避免临界点问题
+    private static final int TOKEN_EXPIRE_BUFFER = 5 ;
+
+    /**
+     * 获取AccessToken
+     * @param companyId 企业ID
+     * @param appId 公众号AppID
+     * @return AccessToken
+     */
+    public String getAccessToken(Integer companyId, String appId) {
+        // 1. 从数据库获取账号信息
+        WechatAccount account = wechatAccountMapper.findByCompanyAndApp(companyId, appId);
+        if (account == null) {
+            throw new RuntimeException("未找到对应的公众号配置");
+        }
+
+        // 2. 检查Token是否有效
+        if (isTokenValid(account)) {
+            return account.getAccessToken();
+        }
+
+        // 3. Token无效则重新获取
+        return refreshAccessToken(account);
+    }
+
+    /**
+     * 判断Token是否有效
+     */
+    private boolean isTokenValid(WechatAccount account) {
+        return account.getAccessToken() != null
+                && account.getTokenExpireTime() != null
+                && account.getTokenExpireTime().isAfter(
+                LocalDateTime.now().plusMinutes(TOKEN_EXPIRE_BUFFER));
+    }
+
+    /**
+     * 刷新AccessToken
+     */
+    private String refreshAccessToken(WechatAccount account) {
+        String url = String.format(
+                "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                account.getAppId(), account.getAppSecret());
+
+        try {
+            System.out.println("url==>"+url);
+            String response = restTemplate.getForObject(url, String.class);
+            JSONObject json = JSON.parseObject(response);
+
+            if (json.containsKey("access_token")) {
+                String newToken = json.getString("access_token");
+                int expiresIn = json.getIntValue("expires_in");
+                LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expiresIn);
+
+                // 更新数据库
+                wechatAccountMapper.updateTokenInfo(
+                        account.getCompanyId(),
+                        account.getAppId(),
+                        newToken,
+                        expireTime);
+
+                return newToken;
+            } else {
+                throw new RuntimeException("获取AccessToken失败: " + response);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("调用微信API失败", e);
+        }
+    }
+}

+ 115 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/util/FileZipUtil.java

@@ -0,0 +1,115 @@
+package com.management.platform.util;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class FileZipUtil {
+
+    /**
+     * 将指定路径下的所有文件打包zip导出
+     * @param response HttpServletResponse
+     * @param sourceFilePath 需要打包的文件夹路径
+     * @param fileName 下载时的文件名称
+     * @param postfix 下载时的文件后缀 .zip/.rar
+     */
+    public static void exportZip(HttpServletResponse response, String sourceFilePath, List<String> fileNames, String fileName, String postfix) {
+        // 默认文件名以时间戳作为前缀
+        if(StringUtils.isBlank(fileName)){
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+            fileName = sdf.format(new Date());
+        }
+        String downloadName = fileName + postfix;
+        // 将文件进行打包下载
+        try {
+            OutputStream os = response.getOutputStream();
+            // 接收压缩包字节
+            byte[] data = createZip(sourceFilePath,fileNames);
+            response.reset();
+            response.setCharacterEncoding("UTF-8");
+            response.addHeader("Access-Control-Allow-Origin", "*");
+            response.setHeader("Access-Control-Expose-Headers", "*");
+            // 下载文件名乱码问题
+            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(downloadName, "UTF-8"));
+            //response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + downloadName);
+            response.addHeader("Content-Length", "" + data.length);
+            response.setContentType("application/octet-stream;charset=UTF-8");
+            IOUtils.write(data, os);
+            os.flush();
+            os.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 创建zip文件
+     * @param sourceFilePath
+     * @return byte[]
+     * @throws Exception
+     */
+    private static byte[] createZip(String sourceFilePath,List<String> fileNames) throws Exception{
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        ZipOutputStream zip = new ZipOutputStream(outputStream);
+        // 将目标文件打包成zip导出
+        File file = new File(sourceFilePath);
+        handlerFile(zip, file,"",fileNames);
+        // 无异常关闭流,它将无条件的关闭一个可被关闭的对象而不抛出任何异常。
+        IOUtils.closeQuietly(zip);
+        return outputStream.toByteArray();
+    }
+
+    /**
+     * 打包处理
+     * @param zip
+     * @param file
+     * @param dir
+     * @throws Exception
+     */
+    private static void handlerFile(ZipOutputStream zip, File file, String dir,List<String> fileNames) throws Exception {
+        // 得到文件列表信息
+        File[] fileArray = file.listFiles();
+
+        if (fileArray == null) {
+            extracted(zip, file, dir);
+        } else {
+            // 过滤出与 fileNames 匹配的文件
+            for (File f : fileArray) {
+                if (fileNames.contains(f.getName())) { // 使用 contains 方法检查
+                    extracted(zip, f, dir + f.getName()); // 打包处理匹配的文件
+                } else if (f.isDirectory()) {
+                    // 递归处理子目录
+                    handlerFile(zip, f, dir + f.getName() + "/", fileNames);
+                }
+            }
+        }
+
+            /*//将文件夹添加到下一级打包目录
+            zip.putNextEntry(new ZipEntry(dir + "/"));
+            dir = dir.length() == 0 ? "" : dir + "/";
+            // 递归将文件夹中的文件打包
+            for (File f : fileArray) {
+                handlerFile(zip, f, dir + f.getName(),fileNames);
+            }*/
+
+    }
+
+    private static void extracted(ZipOutputStream zip, File file, String dir) throws IOException {
+        // 如果当前的是文件,打包处理
+        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+        ZipEntry entry = new ZipEntry(dir); // 使用完整路径(包含文件名)
+        zip.putNextEntry(entry);
+        zip.write(FileUtils.readFileToByteArray(file));
+        IOUtils.closeQuietly(bis);
+        zip.closeEntry(); // 关闭当前条目
+    }
+}

+ 12 - 3
fhKeeper/formulahousekeeper/management-crm/src/main/resources/application.yml

@@ -1,5 +1,5 @@
 server:
-  port: 10010
+  port: 10011
   tomcat:
     uri-encoding: utf-8
     max-http-form-post-size: -1
@@ -15,7 +15,7 @@ spring:
       location: C:/upload/
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://1.94.62.58:17089/man_crm_bk?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&useSSL=false
+    url: jdbc:mysql://1.94.62.58:17089/man_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&useSSL=false
     username: root
     password: P011430@Huoshi*
 
@@ -171,4 +171,13 @@ supersonic:
 
 aiask:
   fileaskurl: http://123.60.32.117:5000/analyze
-  askurl: http://123.60.32.117:5000/analyzeOnlyQuestion
+  askurl: http://123.60.32.117:5000/analyzeOnlyQuestion
+
+  #临时的整型参数值
+weixin:
+  ticket:
+    action_name:
+     QR_SCENE: QR_SCENE
+     QR_STR_SCENE: QR_STR_SCENE
+     QR_LIMIT_SCENE: QR_LIMIT_SCENE
+     QR_LIMIT_STR_SCENE: QR_LIMIT_STR_SCENE

+ 20 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/UserQrCodeMapper.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.management.platform.mapper.UserQrCodeMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.management.platform.entity.UserQrCode">
+        <id column="id" property="id" />
+        <result column="user_id" property="userId" />
+        <result column="company_id" property="companyId" />
+        <result column="ticket" property="ticket" />
+        <result column="url" property="url" />
+        <result column="img" property="img" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, user_id, company_id, ticket, url, img
+    </sql>
+
+</mapper>

+ 22 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/WechatAccountMapper.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.management.platform.mapper.WechatAccountMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.management.platform.entity.WechatAccount">
+        <id column="id" property="id" />
+        <result column="company_id" property="companyId" />
+        <result column="app_id" property="appId" />
+        <result column="app_secret" property="appSecret" />
+        <result column="access_token" property="accessToken" />
+        <result column="token_expire_time" property="tokenExpireTime" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, company_id, app_id, app_secret, access_token, token_expire_time, create_time, update_time
+    </sql>
+
+</mapper>