feat(fsgx): HJF queue merge, brokerage timing, cycle commission, points release

- Add HJF jobs, services, DAOs, models, admin/API controllers, release command
- Respect brokerage_timing (on_pay vs confirm); dispatch HjfOrderPayJob for queue goods
- Queue-only cycle commission and position index fix in StoreOrderCreateServices
- UserBill income types: frozen_points_brokerage, frozen_points_release
- Timer: fsgx_release_frozen_points -> PointsReleaseServices
- Agent tasks: no_assess filtering for direct/umbrella counts
- Migrations: queue_pool, points_release_log, fsgx_v1 checklist updates
- Admin/uniapp: crontab preset, membership level, user list, finance routes, docs

Made-with: Cursor
This commit is contained in:
apple
2026-03-24 11:59:09 +08:00
parent 434aa8c69d
commit 76ccb24679
59 changed files with 2902 additions and 237 deletions

View File

@@ -3,7 +3,7 @@ NODE_ENV=production
VUE_APP_ENV='production'
# 页面 title
VUE_APP_TITLE=CRMEB
VUE_APP_TITLE=fsgx-shop
# socket 系统连接地址 (ws)或(wss)://www.crmeb.com(换成你的域名)/ws 非独立部署默认为空
VUE_APP_WS_ADMIN_URL='ws://fsgx.uj345.com/ws'
# 接口请求地址 (http)或 (https)://www.crmeb.com(换成你的域名)/adminapi 非独立部署默认为空

View File

@@ -679,6 +679,17 @@ export function timerTask() {
});
}
/**
* 手动立即触发定时任务
* @param {number} id
* @returns
*/
export function runTimerNow(id) {
return request({
url: `system/timer/run_now/${id}`,
});
}
export function systemFormData(id, params) {
return request({
url: `/system/form/data/${id}`,

View File

@@ -63,6 +63,11 @@
baseURL: Setting.apiBaseURL.replace(/adminapi/, '')
};
},
mounted() {
if (Array.isArray(this.valueModel) || (typeof this.valueModel === 'object' && this.valueModel !== null)) {
this.valueModel = JSON.stringify(this.valueModel);
}
},
methods: {
handleCopySuccess(){
this.$Message.success('复制成功');

View File

@@ -27,7 +27,10 @@
name: "radioBuild",
mixins:[build, components],
mounted() {
this.valueModel = parseFloat(this.valueModel);
const parsed = parseFloat(this.valueModel);
if (!isNaN(parsed) && String(parsed) === String(this.valueModel)) {
this.valueModel = parsed;
}
},
components: {
useComponent:() => import('./useComponent'),

View File

@@ -19,7 +19,7 @@
</Form>
<Drawer v-model="guideShow" :title="`${title}引导`" width="800">
<component :is="types"></component>
<component :is="types" v-if="guideComponents.includes(types)"></component>
</Drawer>
</div>
</template>
@@ -91,8 +91,13 @@
loading: false,
errorsValidate: [],
title: this.$parent.title,
types: this.$parent.typeMole || this.$parent.type,
types: (() => {
const t = this.$parent.typeMole || this.$parent.type;
const allowed = ['app', 'routine', 'wechat', 'work'];
return allowed.includes(t) ? t : (this.$parent.type || '');
})(),
guideShow: false,
guideComponents: ['app', 'routine', 'wechat', 'work'],
};
},
watch: {

View File

@@ -6,18 +6,6 @@ const chalk = require("chalk");
console.log(
chalk
.hex("#DEADED")
.underline("😄 Hello ~ 欢迎使用CRMEB Pro版我们将竭诚为您服务")
);
console.log(
chalk.yellow("[提示] 点击这里可以我们的更多产品~ ") +
chalk.blue.underline("https://www.crmeb.com")
);
console.log(
chalk.yellow("[提示] 点击这里可以查看开发文档喔~ ") +
chalk.blue.underline("https://doc.crmeb.com")
);
console.log(
chalk.yellow("[提示] 点击这里可以进入我们的论坛社区~ ") +
chalk.blue.underline("https://www.crmeb.com/ask")
.underline("😄 Hello ~ 欢迎使用Pro版我们将竭诚为您服务")
);
console.log(chalk.blue("info - [你知道吗?] 按 Ctrl+C 可以停止服务呢~"));

View File

@@ -101,6 +101,7 @@ export const defaultObj = {
is_sub: 0, //返佣方式
is_vip: 0,
level_type: 1, //默认比例 自定义
is_queue_goods: 0, //是否报单商品1=是0=否
};
export const GoodsTableHead = [

View File

@@ -239,6 +239,16 @@ export default {
minWidth: 130,
title: "二级返佣比例(上浮后)(%)",
},
{
key: "direct_reward_points",
minWidth: 120,
title: "直推奖励积分",
},
{
key: "umbrella_reward_points",
minWidth: 120,
title: "伞下奖励积分",
},
{
slot: "status",
minWidth: 80,

View File

@@ -29,6 +29,10 @@
>编辑</router-link
>
<Divider type="vertical" />
<a @click="handleRunNow(row)" :class="{ 'running': row._running }">
{{ row._running ? '执行中…' : '手动触发' }}
</a>
<Divider type="vertical" />
<a @click="handleDelete(row, '删除定时任务', index)">删除</a>
</template>
</Table>
@@ -46,7 +50,7 @@
</template>
<script>
import { timerIndex, showTimer } from '@/api/system';
import { timerIndex, showTimer, runTimerNow } from '@/api/system';
export default {
name: 'system_crontab',
@@ -79,7 +83,7 @@ export default {
slot: 'action',
align: 'center',
fixed: 'right',
minWidth: 100,
minWidth: 200,
},
],
tableData: [],
@@ -133,6 +137,21 @@ export default {
this.$Message.error(res.msg);
});
},
// 手动立即触发
handleRunNow(row) {
this.$set(row, '_running', true);
runTimerNow(row.id)
.then((res) => {
this.$Message.success(res.msg || '触发成功,任务已执行');
this.getList();
})
.catch((res) => {
this.$Message.error(res.msg || '触发失败');
})
.finally(() => {
this.$set(row, '_running', false);
});
},
// 是否开启
handleChange({ id, is_open }) {
showTimer(id, is_open)
@@ -152,4 +171,9 @@ export default {
};
</script>
<style lang="stylus" scoped></style>
<style lang="stylus" scoped>
.running
color #999
cursor not-allowed
pointer-events none
</style>

View File

@@ -54,15 +54,15 @@
<div class="iconfont iconxiayi"></div>
</div>
</FormItem>
<FormItem label="会员等级:" label-for="hjf_member_level">
<FormItem label="分销等级:" label-for="hjf_member_level">
<Select
v-model="userFrom.hjf_member_level"
placeholder="请选择"
placeholder="请选择分销等级"
element-id="hjf_member_level"
clearable
class="input-add"
>
<Option :value="0">普通会员</Option>
<Option :value="0">普通无分销等级</Option>
<Option :value="1">创客</Option>
<Option :value="2">云店</Option>
<Option :value="3">服务商</Option>
@@ -420,14 +420,15 @@
</div>
</template>
</vxe-column>
<vxe-column field="member_level" title="HJF等级" min-width="110">
<vxe-column field="member_level_name" title="分销等级" min-width="160">
<template v-slot="{ row }">
<HjfMemberBadge
v-if="row.member_level != null"
v-if="row.member_level != null && row.member_level > 0"
:level="Number(row.member_level)"
:level-name="row.member_level_name || ''"
size="small"
/>
<span v-else class="level-none">普通会员</span>
<span v-else class="level-none">普通无等级</span>
</template>
</vxe-column>
<vxe-column field="direct_count" title="直推人数" min-width="90">
@@ -452,22 +453,22 @@
title="推荐人"
min-width="100"
></vxe-column>
<vxe-column field="available_points" title="可用积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.available_points != null ? row.available_points : 0 }}</span>
</template>
</vxe-column>
<vxe-column field="frozen_points" title="待释放(冻结)积分" min-width="150">
<template v-slot="{ row }">
<span>{{ row.frozen_points != null ? row.frozen_points : 0 }}</span>
</template>
</vxe-column>
<vxe-column field="now_money" title="余额" min-width="100"></vxe-column>
<vxe-column field="frozen_points" title="待释放积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.frozen_points != null ? row.frozen_points : '-' }}</span>
</template>
</vxe-column>
<vxe-column field="available_points" title="已释放积分" min-width="100">
<template v-slot="{ row }">
<span>{{ row.available_points != null ? row.available_points : '-' }}</span>
</template>
</vxe-column>
<vxe-column
field="action"
title="操作"
align="center"
width="240"
width="180"
fixed="right"
>
<template v-slot="{ row }">
@@ -475,8 +476,6 @@
<Divider type="vertical" />
<a @click="changeMenu(row, '10')">编辑</a>
<Divider type="vertical" />
<a @click="openLevelModal(row)">调整等级</a>
<Divider type="vertical" />
<a @click="handleNoAssess(row)" :style="row.no_assess ? 'color:#19be6b' : 'color:#ed4014'">
{{ row.no_assess ? '取消不考核' : '不考核' }}
</a>
@@ -791,7 +790,7 @@ import {
exportUserData,
} from "@/api/user";
import { agentSpreadApi } from "@/api/agent";
import { memberList, memberSetLevel, memberSetNoAssess } from "@/api/hjfMember";
import { memberList, memberSetLevel, memberSetNoAssess } from "@/api/hjfMember"; // memberSetNoAssess 仍保留供 handleNoAssess 使用
import editFrom from "../../../components/from/from";
import sendFrom from "@/components/sendCoupons/index";
import userDetails from "./handle/userDetails";
@@ -881,7 +880,7 @@ export default {
group_id: "",
field_key: "",
is_channel: "",
/** 会员等级筛选(HJF 扩展字段0 普通 1 创客 2 云店 3 服务商 4 分公司,空串表示全部 */
/** 分销等级筛选(对应 eb_agent_level.grade / eb_user.agent_level,空串表示全部 */
hjf_member_level: "",
},
field_key: "",
@@ -1455,12 +1454,13 @@ export default {
this.userFrom.group_id === "all" ? "" : this.userFrom.group_id;
userList(this.userFrom)
.then(async (res) => {
let data = res.data;
data.list.forEach((item) => {
const data = res.data || {};
const rows = Array.isArray(data.list) ? data.list : [];
rows.forEach((item) => {
item.checkBox = false;
});
this.userLists = data.list;
this.total = data.count;
this.userLists = rows;
this.total = data.count != null ? data.count : 0;
this.loading = false;
this.$nextTick(function () {
if (this.isAll == 1) {

View File

@@ -59,5 +59,17 @@ export default {
},
component: () => import('@/pages/finance/commission/index')
},
{
path: 'extract_setting',
name: `${pre}extractSetting`,
meta: {
auth: ['finance-extract-setting'],
title: '提现设置'
},
component: () => import('@/components/fromSubmit/commonForm.vue'),
props: {
typeMole: 'advance'
}
},
]
};

View File

@@ -20,8 +20,9 @@
<button form-type="submit" class="confirmBnt bg-color">确认修改</button>
</form>
</view>
<Verify @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
ref="verify"></Verify>
<!-- 安全验证组件保留备用当前修改密码流程已跳过 -->
<!-- <Verify @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
ref="verify"></Verify> -->
<!-- #ifdef MP -->
<authorize v-if="isShowAuth" @authColse="authColse" @onLoadFun="onLoadFun"></authorize>
<!-- #endif -->
@@ -120,53 +121,66 @@
that.phone = phone;
});
},
success(data) {
console.log(data,'data');
this.$refs.verify.hide()
getCodeApi()
.then(res => {
this.keyCode = res.data.key;
this.getCode(data);
})
.catch(res => {
this.$util.Tips({
title: res
});
success(data) {
this.$refs.verify.hide()
getCodeApi()
.then(res => {
this.keyCode = res.data.key;
this.getCode(data);
})
.catch(res => {
this.$util.Tips({
title: res
});
},
/**
* 发送验证码
*
*/
code(data) {
let that = this;
if (!that.userInfo.phone) return that.$util.Tips({
title: '手机号码不存在,无法发送验证码!'
});
this.$refs.verify.show()
},
async getCode(data){
let that = this;
await registerVerify({
phone: that.userInfo.phone,
type: 'reset',
key: that.key,
captchaType: CAPTCHA_TYPE,
captchaVerification: data.captchaVerification,
})
.then(res => {
that.$util.Tips({
title: res.msg
});
that.sendCode();
})
.catch(res => {
this.$refs.verify.refresh()
that.$util.Tips({
title: res
});
},
/**
* 发送验证码(直接发送,不弹安全验证)
*/
code() {
let that = this;
if (!that.userInfo.phone) return that.$util.Tips({
title: '手机号码不存在,无法发送验证码!'
});
registerVerify({
phone: that.userInfo.phone,
type: 'reset',
key: that.key,
})
.then(res => {
that.$util.Tips({
title: res.msg
});
},
that.sendCode();
})
.catch(res => {
that.$util.Tips({
title: res
});
});
},
async getCode(data){
let that = this;
await registerVerify({
phone: that.userInfo.phone,
type: 'reset',
key: that.key,
captchaType: CAPTCHA_TYPE,
captchaVerification: data.captchaVerification,
})
.then(res => {
that.$util.Tips({
title: res.msg
});
that.sendCode();
})
.catch(res => {
this.$refs.verify.refresh()
that.$util.Tips({
title: res
});
});
},
/**
* H5登录 修改密码
*