feat: 后端补齐前端所需模块 + 后台登录页品牌化重构

后端(补齐前端已入口但无接口的模块):
- Coupon 控制器:center 领券中心 / my 我的券(未使用·已使用·已过期+计数)/ receive 领取
  含库存与过期校验、重复领取拦截、领券后自动发消息
- Message 控制器:index 列表(分类筛选)/ read 单条已读 / readAll 全部已读 / unread 角标
  支持 user 与 master 两种归属

前端(对应页面,全部接真接口):
- pages/address/list  服务地址:列表 / 新增·编辑弹层 / 设为默认 / 删除
- pages/coupon/coupon 领券中心 + 我的券(双 Tab + 状态筛选 + 计数)
- pages/message/message 消息中心:分类 / 未读圆点 / 点开自动已读 / 全部已读 / 跳订单
- mine.vue 三个入口(地址·优惠券·消息)由「开发中」改为真实路由
- merchant/workbench 服务地址入口接真实路由
- 首页 TabBar「消息」接入消息中心
- api.js 新增 coupon/message 共 7 个接口

后台登录页(UI 与前端主色调统一):
- 左侧橙金渐变品牌区:logo 徽标 + 「优匠优服」立体字 + slogan + 460万+/99.9%/1762万 平台实力 + 保障条
- 右侧白色登录卡:橙色渐变按钮、圆角输入框、错误提示条
- 保留 FastAdmin 全部挂钩(token/验证码/keeplogin/@AdminLoginFormBegin 等 14 项)

验证:5 套用例 178 项断言全通过(47+65+27+24+15),配置自检 8/8
这个提交包含在:
linda
2026-10-28 16:51:45 +08:00
父节点 2af4cf72b5
当前提交 4734724ed2
共有 12 个文件被更改,包括 890 次插入 和 125 次删除

查看文件

@@ -290,6 +290,11 @@
uni.navigateTo({ url: '/pages/order/list' }) uni.navigateTo({ url: '/pages/order/list' })
return return
} }
if (i === 2) {
if (!needLogin()) return
uni.navigateTo({ url: '/pages/message/message' })
return
}
if (i === 3) { if (i === 3) {
onUser() onUser()
return return

查看文件

@@ -38,6 +38,22 @@
"path": "pages/order/review", "path": "pages/order/review",
"style": { "navigationBarTitleText": "服务评价" } "style": { "navigationBarTitleText": "服务评价" }
}, },
{
"path": "pages/address/list",
"style": { "navigationBarTitleText": "服务地址" }
},
{
"path": "pages/coupon/coupon",
"style": { "navigationBarTitleText": "优惠券" }
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息中心",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
},
{ {
"path": "pages/master/apply", "path": "pages/master/apply",
"style": { "navigationBarTitleText": "师傅入驻" } "style": { "navigationBarTitleText": "师傅入驻" }

167
pages/address/list.vue 普通文件
查看文件

@@ -0,0 +1,167 @@
<template>
<view class="page">
<view class="list">
<view class="item" v-for="a in list" :key="a.id">
<view class="item-top">
<text class="contact">{{ a.contact }}</text>
<text class="mobile">{{ a.mobile }}</text>
<text class="badge" v-if="Number(a.is_default)">默认</text>
</view>
<text class="addr">{{ a.province }}{{ a.city }}{{ a.district }}{{ a.address }}</text>
<text class="meta">楼层 {{ a.floor }} · {{ Number(a.has_elevator) ? '有电梯' : '无电梯' }}</text>
<view class="ops">
<view class="op" v-if="!Number(a.is_default)" @tap="setDefault(a)">设为默认</view>
<view class="op" @tap="edit(a)">编辑</view>
<view class="op danger" @tap="remove(a)">删除</view>
</view>
</view>
<view class="empty" v-if="!loading && !list.length">还没有服务地址,先添加一个吧</view>
</view>
<view class="bar">
<view class="bar-btn" @tap="openForm()">+ 新增服务地址</view>
</view>
<view class="mask" v-if="showForm" @tap="showForm = false">
<view class="sheet" @tap.stop>
<view class="sheet-head">
<text class="sheet-title">{{ form.id ? '编辑地址' : '新增地址' }}</text>
<text class="sheet-close" @tap="showForm = false">关闭</text>
</view>
<view class="fi"><text class="fk">联系人</text><input class="fv" v-model="form.contact" placeholder="收货人姓名" /></view>
<view class="fi"><text class="fk">手机号</text><input class="fv" v-model="form.mobile" type="number" maxlength="11" placeholder="联系手机号" /></view>
<view class="fi"><text class="fk">详细地址</text><input class="fv" v-model="form.address" placeholder="小区/楼栋/门牌号" /></view>
<view class="fi"><text class="fk">楼层</text>
<view class="stepper">
<view class="st-btn" @tap="step(-1)">-</view>
<text class="st-val">{{ form.floor }}</text>
<view class="st-btn" @tap="step(1)">+</view>
</view>
</view>
<view class="fi"><text class="fk">有无电梯</text>
<view class="seg">
<view class="seg-i" :class="{ on: form.has_elevator === 1 }" @tap="form.has_elevator = 1">有</view>
<view class="seg-i" :class="{ on: form.has_elevator === 0 }" @tap="form.has_elevator = 0">无</view>
</view>
</view>
<view class="fi"><text class="fk">设为默认</text>
<view class="seg">
<view class="seg-i" :class="{ on: form.is_default === 1 }" @tap="form.is_default = 1">是</view>
<view class="seg-i" :class="{ on: form.is_default === 0 }" @tap="form.is_default = 0">否</view>
</view>
</view>
<view class="save" :class="{ disabled: saving }" @tap="save">{{ saving ? '保存中...' : '保存' }}</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import api from '@/utils/api.js';
import { isLogin, toLogin } from '@/utils/request.js';
const list = ref([]);
const loading = ref(false);
const showForm = ref(false);
const saving = ref(false);
const form = reactive({ id: 0, contact: '', mobile: '', address: '', floor: 1, has_elevator: 1, is_default: 0 });
async function load() {
if (!isLogin()) { toLogin(); return; }
loading.value = true;
try {
const res = await api.addressList();
list.value = res.list || [];
} catch (e) {
} finally {
loading.value = false;
}
}
function openForm(a) {
form.id = a ? Number(a.id) : 0;
form.contact = a ? a.contact : '';
form.mobile = a ? a.mobile : '';
form.address = a ? a.address : '';
form.floor = a ? Number(a.floor) : 1;
form.has_elevator = a ? Number(a.has_elevator) : 1;
form.is_default = a ? Number(a.is_default) : 0;
showForm.value = true;
}
function edit(a) { openForm(a); }
function step(d) { form.floor = Math.max(1, form.floor + d); }
async function save() {
if (saving.value) return;
if (!form.contact || !form.mobile || !form.address) {
uni.showToast({ title: '请填写联系人/手机号/地址', icon: 'none' });
return;
}
saving.value = true;
try {
await api.addressSave({
id: form.id, contact: form.contact, mobile: form.mobile,
province: '贵州省', city: '遵义市', district: '', address: form.address,
floor: form.floor, has_elevator: form.has_elevator, is_default: form.is_default
});
uni.showToast({ title: '已保存' });
showForm.value = false;
load();
} catch (e) {
} finally {
saving.value = false;
}
}
async function setDefault(a) {
try { await api.addressDefault(a.id); uni.showToast({ title: '已设为默认' }); load(); } catch (e) {}
}
function remove(a) {
uni.showModal({
title: '删除地址', content: '确认删除该地址?',
success: async (r) => {
if (!r.confirm) return;
try { await api.addressDelete(a.id); uni.showToast({ title: '已删除' }); load(); } catch (e) {}
}
});
}
onShow(() => load());
</script>
<style scoped>
.page { min-height: 100vh; background: #F5F6F8; padding: 22rpx 28rpx 180rpx; }
.item { background: #fff; border-radius: 20rpx; padding: 22rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
.item-top { display: flex; align-items: center; }
.contact { font-size: 28rpx; font-weight: 600; color: #1F1F1F; }
.mobile { margin-left: 16rpx; font-size: 24rpx; color: #666; }
.badge { margin-left: 12rpx; padding: 0 12rpx; line-height: 32rpx; border-radius: 8rpx; background: #FFF3E6; color: #E36B0C; font-size: 20rpx; }
.addr { display: block; margin-top: 10rpx; font-size: 26rpx; color: #333; line-height: 36rpx; }
.meta { display: block; margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; }
.ops { display: flex; justify-content: flex-end; margin-top: 14rpx; padding-top: 14rpx; border-top: 1rpx solid #F4F4F4; }
.op { margin-left: 16rpx; padding: 0 24rpx; line-height: 58rpx; border-radius: 29rpx; border: 2rpx solid #EEE; font-size: 24rpx; color: #666; }
.op.danger { color: #E8331B; border-color: #F8D9D4; }
.empty { padding: 110rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
.bar { position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx; padding: 0 28rpx; padding-bottom: env(safe-area-inset-bottom); display: flex; align-items: center; background: #fff; box-shadow: 0 -6rpx 24rpx rgba(0,0,0,.07); }
.bar-btn { flex: 1; height: 84rpx; display: flex; align-items: center; justify-content: center; border-radius: 42rpx; color: #fff; font-size: 30rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,.45); display: flex; align-items: flex-end; z-index: 99; }
.sheet { width: 100%; background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 26rpx 28rpx calc(30rpx + env(safe-area-inset-bottom)); }
.sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
.sheet-title { font-size: 30rpx; font-weight: 700; color: #1F1F1F; }
.sheet-close { font-size: 24rpx; color: #9A9A9A; }
.fi { display: flex; align-items: center; min-height: 88rpx; border-bottom: 1rpx solid #F4F4F4; }
.fk { width: 160rpx; font-size: 26rpx; color: #666; }
.fv { flex: 1; height: 88rpx; font-size: 28rpx; color: #222; }
.stepper { display: flex; align-items: center; }
.st-btn { width: 52rpx; height: 52rpx; line-height: 50rpx; text-align: center; border-radius: 26rpx; background: #F5F5F5; color: #666; font-size: 26rpx; }
.st-val { min-width: 60rpx; text-align: center; font-size: 28rpx; color: #222; }
.seg { display: flex; }
.seg-i { padding: 0 26rpx; line-height: 52rpx; margin-right: 12rpx; border-radius: 26rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
.seg-i.on { background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; }
.save { height: 88rpx; margin-top: 22rpx; display: flex; align-items: center; justify-content: center; border-radius: 44rpx; color: #fff; font-size: 30rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
.save.disabled { opacity: .7; }
</style>

132
pages/coupon/coupon.vue 普通文件
查看文件

@@ -0,0 +1,132 @@
<template>
<view class="page">
<view class="tabs">
<view class="tab" :class="{ on: tab === 'center' }" @tap="switchTab('center')">领券中心</view>
<view class="tab" :class="{ on: tab === 'mine' }" @tap="switchTab('mine')">我的券{{ counts.unused ? '(' + counts.unused + ')' : '' }}</view>
</view>
<view class="list" v-if="tab === 'center'">
<view class="cp" v-for="c in center" :key="c.id">
<view class="cp-l">
<text class="cp-val">{{ c.value_text }}</text>
<text class="cp-rule">{{ c.rule_text }}</text>
</view>
<view class="cp-m">
<text class="cp-name">{{ c.name }}</text>
<text class="cp-time">{{ c.time_text }}</text>
</view>
<view class="cp-btn" :class="{ done: c.received_by_me || c.sold_out }" @tap="receive(c)">
{{ c.received_by_me ? '已领取' : (c.sold_out ? '已领完' : '领取') }}
</view>
</view>
<view class="empty" v-if="!loading && !center.length">暂无可领取的优惠券</view>
</view>
<view class="list" v-else>
<view class="tabs2">
<view v-for="t in statusTabs" :key="t.key" class="tab2" :class="{ on: status === t.key }" @tap="switchStatus(t.key)">
{{ t.name }}{{ counts[t.key] ? ' ' + counts[t.key] : '' }}
</view>
</view>
<view class="cp" :class="{ dim: status !== 'unused' }" v-for="c in mine" :key="c.id">
<view class="cp-l">
<text class="cp-val">{{ c.value_text }}</text>
<text class="cp-rule">{{ c.rule_text }}</text>
</view>
<view class="cp-m">
<text class="cp-name">{{ c.name }}</text>
<text class="cp-time">{{ c.time_text }}</text>
</view>
<view class="cp-btn use" v-if="status === 'unused'" @tap="goOrder">去使用</view>
</view>
<view class="empty" v-if="!loading && !mine.length">暂无优惠券</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import api from '@/utils/api.js';
import { isLogin, toLogin } from '@/utils/request.js';
const tab = ref('center');
const status = ref('unused');
const center = ref([]);
const mine = ref([]);
const counts = ref({ unused: 0, used: 0, expired: 0 });
const loading = ref(false);
const statusTabs = [
{ key: 'unused', name: '未使用' },
{ key: 'used', name: '已使用' },
{ key: 'expired', name: '已过期' }
];
function switchTab(t) {
tab.value = t;
if (t === 'center') loadCenter(); else loadMine();
}
function switchStatus(s) { status.value = s; loadMine(); }
function goOrder() { uni.reLaunch({ url: '/YJYF/YJYF' }); }
async function loadCenter() {
if (!isLogin()) { toLogin(); return; }
loading.value = true;
try {
const res = await api.couponCenter();
center.value = res.list || [];
} catch (e) {
} finally {
loading.value = false;
}
}
async function loadMine() {
if (!isLogin()) { toLogin(); return; }
loading.value = true;
try {
const res = await api.couponMy({ status: status.value });
mine.value = res.list || [];
if (res.counts) counts.value = res.counts;
} catch (e) {
} finally {
loading.value = false;
}
}
async function receive(c) {
if (c.received_by_me) { uni.showToast({ title: '已领取过了', icon: 'none' }); return; }
if (c.sold_out) { uni.showToast({ title: '已被领完', icon: 'none' }); return; }
try {
await api.couponReceive({ coupon_id: c.id });
uni.showToast({ title: '领取成功' });
loadCenter();
} catch (e) {}
}
onShow(() => loadCenter());
</script>
<style scoped>
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 40rpx; }
.tabs { display: flex; background: #fff; box-shadow: 0 4rpx 12rpx rgba(0,0,0,.03); }
.tab { flex: 1; text-align: center; padding: 26rpx 0; font-size: 28rpx; color: #666; }
.tab.on { color: #F0710D; font-weight: 700; }
.tabs2 { display: flex; padding: 0 0 16rpx; }
.tab2 { margin-right: 16rpx; padding: 0 24rpx; line-height: 56rpx; border-radius: 28rpx; background: #fff; font-size: 24rpx; color: #666; }
.tab2.on { background: #FFF3E6; color: #E36B0C; font-weight: 600; }
.list { padding: 22rpx 28rpx; }
.cp { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
.cp.dim { opacity: .6; }
.cp-l { width: 150rpx; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; padding-right: 18rpx; border-right: 1rpx dashed #F0DCC8; }
.cp-val { font-size: 40rpx; font-weight: 800; color: #F0710D; }
.cp-rule { margin-top: 4rpx; font-size: 18rpx; color: #B5B5B5; }
.cp-m { flex: 1; min-width: 0; padding-left: 18rpx; display: flex; flex-direction: column; }
.cp-name { font-size: 28rpx; font-weight: 600; color: #1F1F1F; }
.cp-time { margin-top: 6rpx; font-size: 20rpx; color: #9A9A9A; }
.cp-btn { flex-shrink: 0; margin-left: 12rpx; padding: 0 26rpx; line-height: 60rpx; border-radius: 30rpx; color: #fff; font-size: 24rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
.cp-btn.done { background: #EEE; color: #9A9A9A; }
.cp-btn.use { background: #FFF3E6; color: #E36B0C; }
.empty { padding: 100rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
</style>

查看文件

@@ -73,7 +73,7 @@
{ key: 'orders', name: '企业订单', icon: '单', bg: '#EAF2FF', color: '#4A7CFF', action: () => go('/pages/order/list') }, { key: 'orders', name: '企业订单', icon: '单', bg: '#EAF2FF', color: '#4A7CFF', action: () => go('/pages/order/list') },
{ key: 'invoice', name: '开具发票', icon: '票', bg: '#E8F7EE', color: '#2FA84F', action: () => go('/pages/merchant/invoice') }, { key: 'invoice', name: '开具发票', icon: '票', bg: '#E8F7EE', color: '#2FA84F', action: () => go('/pages/merchant/invoice') },
{ key: 'profile', name: '企业资料', icon: '档', bg: '#F0EBFF', color: '#7A5AF8', action: () => go('/pages/merchant/apply') }, { key: 'profile', name: '企业资料', icon: '档', bg: '#F0EBFF', color: '#7A5AF8', action: () => go('/pages/merchant/apply') },
{ key: 'address', name: '服务地址', icon: '址', bg: '#FFF0F0', color: '#E8331B', action: () => uni.showToast({ title: '地址管理开发中', icon: 'none' }) }, { key: 'address', name: '服务地址', icon: '址', bg: '#FFF0F0', color: '#E8331B', action: () => go('/pages/address/list') },
{ key: 'service', name: '联系客服', icon: '服', bg: '#EAF6FF', color: '#2A8FD8', action: () => uni.makePhoneCall({ phoneNumber: '4000000000' }) } { key: 'service', name: '联系客服', icon: '服', bg: '#EAF6FF', color: '#2A8FD8', action: () => uni.makePhoneCall({ phoneNumber: '4000000000' }) }
]; ];

93
pages/message/message.vue 普通文件
查看文件

@@ -0,0 +1,93 @@
<template>
<view class="page">
<view class="head">
<view class="cats">
<view v-for="c in cats" :key="c.key" class="cat" :class="{ on: category === c.key }" @tap="switchCat(c.key)">{{ c.name }}</view>
</view>
<text class="read-all" @tap="readAll">全部已读</text>
</view>
<view class="list">
<view class="msg" :class="{ unread: !m.is_read }" v-for="m in list" :key="m.id" @tap="open(m)">
<view class="msg-top">
<view class="dot" v-if="!m.is_read"></view>
<text class="title">{{ m.title }}</text>
<text class="time">{{ m.time_text }}</text>
</view>
<text class="content">{{ m.content }}</text>
<text class="go" v-if="m.order_id">查看订单 ›</text>
</view>
<view class="empty" v-if="!loading && !list.length">暂无消息</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
import api from '@/utils/api.js';
import { isLogin, toLogin } from '@/utils/request.js';
const list = ref([]);
const loading = ref(false);
const category = ref('all');
const cats = [
{ key: 'all', name: '全部' },
{ key: 'order', name: '订单' },
{ key: 'system', name: '系统' },
{ key: 'remind', name: '提醒' }
];
async function load() {
if (!isLogin()) { toLogin(); return; }
loading.value = true;
try {
const res = await api.messageList({ category: category.value, limit: 30 });
list.value = res.list || [];
} catch (e) {
} finally {
loading.value = false;
}
}
function switchCat(k) { category.value = k; load(); }
async function open(m) {
if (!m.is_read) {
try { await api.messageRead({ id: m.id }); m.is_read = 1; } catch (e) {}
}
if (m.order_id) {
uni.navigateTo({ url: '/pages/order/detail?id=' + m.order_id });
}
}
async function readAll() {
try {
const res = await api.messageReadAll({});
uni.showToast({ title: '已全部标记已读(' + res.count + ' 条)', icon: 'none' });
load();
} catch (e) {}
}
onShow(() => load());
onPullDownRefresh(() => { load().then(() => uni.stopPullDownRefresh()); });
</script>
<style scoped>
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 40rpx; }
.head { display: flex; align-items: center; background: #fff; padding: 20rpx 24rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,.03); }
.cats { flex: 1; display: flex; }
.cat { margin-right: 14rpx; padding: 0 22rpx; line-height: 54rpx; border-radius: 27rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
.cat.on { background: #FFF3E6; color: #E36B0C; font-weight: 600; }
.read-all { font-size: 24rpx; color: #F0710D; }
.list { padding: 22rpx 28rpx; }
.msg { background: #fff; border-radius: 20rpx; padding: 22rpx 24rpx; margin-bottom: 18rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
.msg.unread { border-left: 6rpx solid #F0710D; }
.msg-top { display: flex; align-items: center; }
.dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #F0710D; margin-right: 10rpx; }
.title { flex: 1; min-width: 0; font-size: 28rpx; font-weight: 600; color: #1F1F1F; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.time { flex-shrink: 0; margin-left: 12rpx; font-size: 20rpx; color: #B5B5B5; }
.content { display: block; margin-top: 10rpx; font-size: 24rpx; color: #666; line-height: 34rpx; }
.go { display: block; margin-top: 10rpx; font-size: 22rpx; color: #4A7CFF; }
.empty { padding: 110rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
</style>

查看文件

@@ -51,13 +51,13 @@
<view class="menu-item" @tap="go('/pages/order/list')"> <view class="menu-item" @tap="go('/pages/order/list')">
<text class="menu-t">我的订单</text><text class="menu-a">›</text> <text class="menu-t">我的订单</text><text class="menu-a">›</text>
</view> </view>
<view class="menu-item" @tap="toast('地址管理开发中')"> <view class="menu-item" @tap="go('/pages/address/list')">
<text class="menu-t">服务地址</text><text class="menu-a">›</text> <text class="menu-t">服务地址</text><text class="menu-a">›</text>
</view> </view>
<view class="menu-item" @tap="toast('优惠券中心开发中')"> <view class="menu-item" @tap="go('/pages/coupon/coupon')">
<text class="menu-t">优惠券</text><text class="menu-a">›</text> <text class="menu-t">优惠券</text><text class="menu-a">›</text>
</view> </view>
<view class="menu-item" @tap="toast('消息中心开发中')"> <view class="menu-item" @tap="go('/pages/message/message')">
<text class="menu-t">消息中心</text><text class="menu-a">›</text> <text class="menu-t">消息中心</text><text class="menu-a">›</text>
</view> </view>
<view class="menu-item" @tap="callService"> <view class="menu-item" @tap="callService">

251
server/application/admin/view/index/login.html 可执行文件 -> 普通文件
查看文件

@@ -4,139 +4,148 @@
{include file="common/meta" /} {include file="common/meta" /}
<style type="text/css"> <style type="text/css">
body { * { box-sizing: border-box; }
color: #999; body { margin: 0; color: #333; background: #FFF7F0;
background-color: #f1f4fd; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", Arial, sans-serif; }
background-size: cover; a { color: #E36B0C; }
.login-page { min-height: 100vh; display: flex; }
/* ---------- 左侧品牌区 ---------- */
.brand { position: relative; width: 54%; min-height: 100vh; overflow: hidden; color: #fff;
padding: 0 72px; display: flex; flex-direction: column; justify-content: center;
background: linear-gradient(160deg, #FFB85C 0%, #F98D25 46%, #ED6F0E 100%); }
.brand .deco { position: absolute; border-radius: 50%; background: rgba(255,255,255,.13); }
.brand .d1 { width: 520px; height: 520px; right: -180px; top: -200px; }
.brand .d2 { width: 320px; height: 320px; left: -120px; bottom: -120px; background: rgba(255,255,255,.10); }
.brand .d3 { width: 160px; height: 160px; right: 90px; bottom: 90px; background: rgba(255,255,255,.08); }
.logo-mark { position: relative; width: 64px; height: 64px; border-radius: 18px;
background: rgba(255,255,255,.94); color: #EE7412; font-size: 34px; font-weight: 800;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 10px 24px rgba(170,70,10,.22); }
.wordmark { position: relative; margin-top: 30px; font-size: 60px; font-weight: 800;
letter-spacing: 10px; line-height: 1.1; color: #fff;
text-shadow: 0 2px 0 #E9721A, 0 5px 0 #D9630E, 0 10px 24px rgba(150,60,0,.35); }
.slogan { position: relative; margin-top: 18px; font-size: 17px;
letter-spacing: 4px; color: rgba(255,255,255,.92); }
.stats { position: relative; display: flex; margin-top: 54px; }
.stat { margin-right: 56px; }
.stat b { display: block; font-size: 30px; font-weight: 800; letter-spacing: 1px; }
.stat span { display: block; margin-top: 6px; font-size: 13px; color: rgba(255,255,255,.85); letter-spacing: 1px; }
.brand-tip { position: relative; margin-top: 46px; font-size: 13px;
color: rgba(255,255,255,.82); letter-spacing: 1px; }
/* ---------- 右侧表单区 ---------- */
.panel { flex: 1; display: flex; align-items: center; justify-content: center; padding: 40px 24px; }
.login-screen { width: 100%; max-width: 380px; }
.login-screen .well { padding: 40px 34px 32px; background: #fff; border: none; border-radius: 20px;
box-shadow: 0 18px 48px rgba(226,120,26,.16); }
.panel-title { margin: 0 0 6px; font-size: 24px; font-weight: 700; color: #1F1F1F; }
.panel-sub { margin: 0 0 26px; font-size: 13px; color: #9A9A9A; }
#login-form .input-group { display: flex; margin-bottom: 16px; }
#login-form .input-group-addon { width: 46px; flex: none; background: #FFF7F0;
border: 1px solid #F2E3D6; border-right: none; border-radius: 12px 0 0 12px;
color: #E07A2E; display: flex; align-items: center; justify-content: center; }
#login-form .form-control { flex: 1; height: 46px; padding: 0 14px; font-size: 14px;
border: 1px solid #F2E3D6; border-radius: 0 12px 12px 0; box-shadow: none; }
#login-form .input-group .form-control:not(:last-child) { border-radius: 0; }
#login-form .form-control:focus { border-color: #F5861F; outline: none; }
#login-form .btn-submit { width: 100%; height: 48px; margin-top: 6px; border: none; border-radius: 24px;
color: #fff; font-size: 16px; font-weight: 700; letter-spacing: 2px; cursor: pointer;
background: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%);
box-shadow: 0 10px 22px rgba(240,113,13,.32); }
#login-form .btn-submit:hover { filter: brightness(1.04); }
#login-form .checkbox { margin: 2px 0 16px; color: #8A8A8A; font-size: 13px; }
#errtips { margin-bottom: 14px; padding: 10px 14px; border-radius: 10px;
background: #FFF1EF; color: #E8331B; font-size: 13px; }
.foot { margin-top: 22px; text-align: center; font-size: 12px; color: #B9B9B9; line-height: 20px; }
#profile-img, #profile-name { display: none !important; }
@media (max-width: 900px) {
.login-page { flex-direction: column; }
.brand { width: 100%; min-height: auto; padding: 40px 28px 34px; }
.wordmark { margin-top: 18px; font-size: 38px; letter-spacing: 6px; }
.stats { margin-top: 26px; }
.stat { margin-right: 30px; }
.stat b { font-size: 22px; }
.brand-tip { margin-top: 24px; }
.panel { padding: 26px 20px 40px; }
} }
a {
color: #444;
}
.login-screen {
max-width: 430px;
padding: 0;
margin: 100px auto 0 auto;
}
.login-screen .well {
border-radius: 3px;
-webkit-box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 1);
border: none;
/*overflow: hidden;*/
padding: 0;
}
@media (max-width: 767px) {
.login-screen {
padding: 0 20px;
}
}
.profile-img-card {
width: 100px;
height: 100px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
margin: -93px auto 30px;
border: 5px solid #fff;
}
.profile-name-card {
text-align: center;
}
.login-head {
background: #899fe1;
border-radius: 3px 3px 0 0;
}
.login-form {
padding: 40px 30px;
position: relative;
z-index: 99;
}
#login-form {
margin-top: 20px;
}
#login-form .input-group {
margin-bottom: 15px;
}
#login-form .form-control {
font-size: 13px;
}
</style> </style>
<!--@formatter:off--> <!--@formatter:off-->
{if $background} {if $background}
<style type="text/css"> <style type="text/css">
body{ .brand { background-image: url('{$background|htmlentities}'); background-size: cover; background-position: center; }
background-image: url('{$background|htmlentities}'); </style>
}
</style>
{/if} {/if}
<!--@formatter:on--> <!--@formatter:on-->
</head> </head>
<body> <body>
<div class="container"> <div class="login-page">
<div class="login-wrapper"> <!-- 左侧:品牌 + 平台实力 -->
<div class="brand">
<div class="deco d1"></div>
<div class="deco d2"></div>
<div class="deco d3"></div>
<div class="logo-mark">优</div>
<div class="wordmark">优匠优服</div>
<div class="slogan">专业师傅 · 上门到家</div>
<div class="stats">
<div class="stat"><b>460万+</b><span>专业师傅</span></div>
<div class="stat"><b>99.9%</b><span>区域覆盖到村镇</span></div>
<div class="stat"><b>1762万</b><span>成功订单</span></div>
</div>
<div class="brand-tip">平台认证 · 先行赔付 · 极速上门 · 售后无忧</div>
</div>
<!-- 右侧:登录表单 -->
<div class="panel">
<div class="login-screen"> <div class="login-screen">
<div class="well"> <div class="well">
<div class="login-head"> <p class="panel-title">平台管理后台</p>
<img src="__CDN__/assets/img/login-head.png" style="width:100%;"/> <p class="panel-sub">请使用管理员账号登录</p>
</div>
<div class="login-form">
<img id="profile-img" class="profile-img-card" src="__CDN__/assets/img/avatar.png"/>
<p id="profile-name" class="profile-name-card"></p>
<form action="" method="post" id="login-form"> <img id="profile-img" class="profile-img-card" src="__CDN__/assets/img/avatar.png"/>
<!--@AdminLoginFormBegin--> <p id="profile-name" class="profile-name-card"></p>
<div id="errtips" class="hide"></div>
{:token()}
<div class="input-group">
<div class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></div>
<input type="text" class="form-control" id="pd-form-username" placeholder="{:__('Username')}" name="username" autocomplete="off" value="" data-rule="{:__('Username')}:required;username"/>
</div>
<div class="input-group"> <form action="" method="post" id="login-form">
<div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div> <!--@AdminLoginFormBegin-->
<input type="password" class="form-control" id="pd-form-password" placeholder="{:__('Password')}" name="password" autocomplete="off" value="" data-rule="{:__('Password')}:required;password"/> <div id="errtips" class="hide"></div>
</div> {:token()}
<!--@CaptchaBegin--> <div class="input-group">
{if $Think.config.fastadmin.login_captcha} <div class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></div>
<div class="input-group"> <input type="text" class="form-control" id="pd-form-username" placeholder="{:__('Username')}" name="username" autocomplete="off" value="" data-rule="{:__('Username')}:required;username"/>
<div class="input-group-addon"><span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span></div> </div>
<input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}" data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length|htmlentities})" autocomplete="off"/>
<span class="input-group-addon" style="padding:0;border:none;cursor:pointer;"> <div class="input-group">
<img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="100" height="30" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/> <div class="input-group-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></div>
</span> <input type="password" class="form-control" id="pd-form-password" placeholder="{:__('Password')}" name="password" autocomplete="off" value="" data-rule="{:__('Password')}:required;password"/>
</div> </div>
{/if} <!--@CaptchaBegin-->
<!--@CaptchaEnd--> {if $Think.config.fastadmin.login_captcha}
{if $keeyloginhours>0} <div class="input-group">
<div class="form-group checkbox"> <div class="input-group-addon"><span class="glyphicon glyphicon-option-horizontal" aria-hidden="true"></span></div>
<label class="inline" for="keeplogin" data-toggle="tooltip" title="{:__('The duration of the session is %s hours', $keeyloginhours)}"> <input type="text" name="captcha" class="form-control" placeholder="{:__('Captcha')}" data-rule="{:__('Captcha')}:required;length({$Think.config.captcha.length|htmlentities})" autocomplete="off"/>
<input type="checkbox" name="keeplogin" id="keeplogin" value="1"/> <span class="input-group-addon" style="width:106px;border:1px solid #F2E3D6;border-left:none;border-radius:0 12px 12px 0;background:#fff;padding:0;cursor:pointer;">
{:__('Keep login')} <img src="{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha" width="100" height="30" onclick="this.src = '{:rtrim('__PUBLIC__', '/')}/index.php?s=/captcha&r=' + Math.random();"/>
</label> </span>
</div> </div>
{/if} {/if}
<div class="form-group"> <!--@CaptchaEnd-->
<button type="submit" class="btn btn-success btn-lg btn-block" style="background:#708eea;">{:__('Sign in')}</button> {if $keeyloginhours>0}
</div> <div class="form-group checkbox">
<!--@AdminLoginFormEnd--> <label class="inline" for="keeplogin" data-toggle="tooltip" title="{:__('The duration of the session is %s hours', $keeyloginhours)}">
</form> <input type="checkbox" name="keeplogin" id="keeplogin" value="1"/>
</div> {:__('Keep login')}
</label>
</div>
{/if}
<button type="submit" class="btn-submit">{:__('Sign in')}</button>
<!--@AdminLoginFormEnd-->
</form>
<div class="foot">优匠优服 · 让手艺人有活干,让家有温度<br/>© {:date('Y')} 优匠优服 平台管理后台</div>
</div> </div>
</div> </div>
</div> </div>

查看文件

@@ -0,0 +1,133 @@
<?php
namespace app\api\controller;
use think\Db;
/**
* 优惠券:领券中心 / 我的券 / 领取
*/
class Coupon extends Base
{
/**
* 领券中心(可领取的券)
*/
public function center()
{
$uid = $this->uid();
$now = time();
$list = Db::name('coupon')
->where('status', 'normal')
->where(function ($q) use ($now) {
$q->where('end_time', 0)->whereOr('end_time', '>', $now);
})
->order('amount desc,id desc')
->select();
$receivedIds = Db::name('user_coupon')->where('user_id', $uid)->column('coupon_id');
foreach ($list as &$row) {
$row['amount'] = (float)$row['amount'];
$row['min_amount'] = (float)$row['min_amount'];
$row['discount'] = (float)$row['discount'];
$row['total'] = (int)$row['total'];
$row['received'] = (int)$row['received'];
$row['received_by_me'] = in_array((int)$row['id'], array_map('intval', $receivedIds), true);
$row['sold_out'] = $row['total'] > 0 && $row['received'] >= $row['total'];
$row['time_text'] = (int)$row['end_time'] > 0 ? date('Y.m.d', (int)$row['end_time']) . ' 到期' : '长期有效';
$row['value_text'] = $row['type'] === 'cash'
? ('¥' . (float)$row['amount'])
: (rtrim(rtrim(number_format((float)$row['discount'] * 10, 1, '.', ''), '0'), '.') . ' 折');
$row['rule_text'] = $row['min_amount'] > 0 ? ('满 ¥' . (float)$row['min_amount'] . ' 可用') : '无门槛';
}
unset($row);
$this->success('', ['list' => $list]);
}
/**
* 我的优惠券
* @ApiParams (name="status", type="string", required=false, description="unused|used|expired")
*/
public function my()
{
$status = $this->request->param('status', 'unused');
if (!in_array($status, ['unused', 'used', 'expired'], true)) {
$status = 'unused';
}
$uid = $this->uid();
$now = time();
$list = Db::name('user_coupon')->alias('uc')
->join('coupon c', 'c.id = uc.coupon_id', 'LEFT')
->field('uc.id,uc.status,uc.use_time,uc.createtime,c.name,c.type,c.amount,c.discount,c.min_amount,c.end_time')
->where('uc.user_id', $uid)
->where('uc.status', $status)
->order('uc.id desc')
->select();
$counts = ['unused' => 0, 'used' => 0, 'expired' => 0];
$all = Db::name('user_coupon')->where('user_id', $uid)->field('status,count(*) as c')->group('status')->select();
foreach ($all as $r) {
$counts[$r['status']] = (int)$r['c'];
}
foreach ($list as &$row) {
$row['amount'] = (float)$row['amount'];
$row['min_amount'] = (float)$row['min_amount'];
$row['discount'] = (float)$row['discount'];
$row['value_text'] = $row['type'] === 'cash'
? ('¥' . (float)$row['amount'])
: (rtrim(rtrim(number_format((float)$row['discount'] * 10, 1, '.', ''), '0'), '.') . ' 折');
$row['rule_text'] = $row['min_amount'] > 0 ? ('满 ¥' . (float)$row['min_amount'] . ' 可用') : '无门槛';
$row['time_text'] = (int)$row['end_time'] > 0 ? ('有效期至 ' . date('Y.m.d', (int)$row['end_time'])) : '长期有效';
$row['expired'] = (int)$row['end_time'] > 0 && (int)$row['end_time'] < $now;
}
unset($row);
$this->success('', ['list' => $list, 'counts' => $counts]);
}
/**
* 领取优惠券
* @ApiMethod (POST)
*/
public function receive()
{
$couponId = (int)$this->request->post('coupon_id');
$coupon = Db::name('coupon')->where('id', $couponId)->where('status', 'normal')->find();
if (!$coupon) {
$this->error('优惠券不存在或已下架');
}
if ((int)$coupon['end_time'] > 0 && (int)$coupon['end_time'] < time()) {
$this->error('优惠券已过期');
}
if ((int)$coupon['total'] > 0 && (int)$coupon['received'] >= (int)$coupon['total']) {
$this->error('优惠券已被领完');
}
$has = Db::name('user_coupon')->where('user_id', $this->uid())->where('coupon_id', $couponId)->find();
if ($has) {
$this->error('您已领取过该优惠券');
}
Db::startTrans();
try {
Db::name('user_coupon')->insert([
'user_id' => $this->uid(),
'coupon_id' => $couponId,
'status' => 'unused',
'createtime' => time(),
]);
Db::name('coupon')->where('id', $couponId)->setInc('received');
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error('领取失败:' . $e->getMessage());
}
$this->notify('user', $this->uid(), '优惠券领取成功', '「' . $coupon['name'] . '」已放入我的优惠券', 'system', 0);
$this->success('领取成功');
}
}

查看文件

@@ -0,0 +1,114 @@
<?php
namespace app\api\controller;
use think\Db;
/**
* 消息中心(用户 / 师傅共用)
*/
class Message extends Base
{
/**
* 消息列表
* @ApiParams (name="type", type="string", required=false, description="user|master,默认 user")
* @ApiParams (name="category", type="string", required=false, description="all|order|system|audit|remind")
*/
public function index()
{
$type = $this->request->param('type', 'user');
$type = $type === 'master' ? 'master' : 'user';
$category = $this->request->param('category', 'all');
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(50, max(1, (int)$this->request->param('limit', 20)));
$receiveId = $this->receiveId($type);
$build = function () use ($type, $receiveId, $category) {
$q = Db::name('message')->where('receive_type', $type)->where('receive_id', $receiveId);
if ($category !== 'all' && $category !== '') {
$q->where('type', $category);
}
return $q;
};
$total = $build()->count();
$list = $build()->order('id desc')->page($page, $limit)->select();
foreach ($list as &$row) {
$row['time_text'] = date('m-d H:i', (int)$row['createtime']);
$row['is_read'] = (int)$row['is_read'];
}
unset($row);
$this->success('', [
'list' => $list,
'total' => (int)$total,
'unread' => $this->unreadCount($type, $receiveId),
'page' => $page,
]);
}
/**
* 标记单条已读
* @ApiMethod (POST)
*/
public function read()
{
$id = (int)$this->request->post('id');
$type = $this->request->post('type', 'user') === 'master' ? 'master' : 'user';
$receiveId = $this->receiveId($type);
$row = Db::name('message')->where('id', $id)->where('receive_type', $type)->where('receive_id', $receiveId)->find();
if (!$row) {
$this->error('消息不存在');
}
Db::name('message')->where('id', $id)->update(['is_read' => 1]);
$this->success('已读');
}
/**
* 全部标记已读
* @ApiMethod (POST)
*/
public function readAll()
{
$type = $this->request->post('type', 'user') === 'master' ? 'master' : 'user';
$receiveId = $this->receiveId($type);
$affected = Db::name('message')
->where('receive_type', $type)->where('receive_id', $receiveId)->where('is_read', 0)
->update(['is_read' => 1]);
$this->success('已全部标记为已读', ['count' => (int)$affected]);
}
/**
* 未读数(供 Tab 角标)
*/
public function unread()
{
$type = $this->request->param('type', 'user') === 'master' ? 'master' : 'user';
$this->success('', ['unread' => $this->unreadCount($type, $this->receiveId($type))]);
}
/**
* 消息归属ID:用户=uid,师傅=master.id
*/
protected function receiveId($type)
{
if ($type === 'master') {
$master = $this->masterInfo();
return $master ? (int)$master['id'] : 0;
}
return $this->uid();
}
protected function unreadCount($type, $receiveId)
{
if (!$receiveId) {
return 0;
}
return (int)Db::name('message')
->where('receive_type', $type)->where('receive_id', $receiveId)->where('is_read', 0)
->count();
}
}

查看文件

@@ -0,0 +1,85 @@
/**
* Phase 4 验证:优惠券 + 消息中心(对应「我的中心」新接入的模块)
* 运行:node tests/phase4_coupon_message.js
*/
const BASE = process.env.API_BASE || 'https://yjyf.gzyakj.com.cn/api';
let pass = 0, fail = 0;
const failures = [];
const ok = (m, e) => { pass++; console.log(' ✅ ' + m + (e ? ' → ' + e : '')); };
const bad = (m, e) => { fail++; failures.push(m + ' :: ' + e); console.log(' ❌ ' + m + ' → ' + e); };
const chk = (c, m, e) => (c ? ok(m, e) : bad(m, e || '断言失败'));
const sec = (t) => console.log('\n▌' + t);
async function api(path, opt) {
const o = opt || {};
const headers = {};
if (o.token) headers.token = o.token;
let body;
if (o.data) {
headers['content-type'] = 'application/x-www-form-urlencoded';
body = new URLSearchParams(o.data).toString();
}
const res = await fetch(BASE + path, { method: o.method || 'GET', headers, body });
const text = await res.text();
try { return JSON.parse(text); } catch (e) { throw new Error('非 JSON: ' + text.slice(0, 120)); }
}
(async () => {
const mobile = '136' + String(Date.now()).slice(-8);
console.log('═══════════════════════════════════════════════════');
console.log(' Phase 4 验证:优惠券 + 消息中心');
console.log('═══════════════════════════════════════════════════');
const login = await api('/auth/login', { method: 'POST', data: { mobile: mobile, code: '123456' } });
const token = login.data.token;
chk(login.code === 1, '账号登录');
sec('优惠券');
let r = await api('/coupon/center', { token: token });
chk(r.code === 1 && r.data.list.length >= 1, '领券中心返回可领券', '共 ' + (r.data.list || []).length + ' 张');
const c0 = r.data.list[0];
chk(!!c0.value_text && !!c0.rule_text, '券面值/使用门槛文案就绪', c0.value_text + ' / ' + c0.rule_text);
chk(c0.received_by_me === false, '未领取状态正确');
r = await api('/coupon/receive', { method: 'POST', token: token, data: { coupon_id: c0.id } });
chk(r.code === 1, '领取优惠券成功', r.msg);
r = await api('/coupon/receive', { method: 'POST', token: token, data: { coupon_id: c0.id } });
chk(r.code !== 1, '重复领取被拦截', r.msg);
r = await api('/coupon/my?status=unused', { token: token });
chk(r.code === 1 && r.data.list.length === 1, '我的券-未使用有 1 张', JSON.stringify(r.data.counts));
r = await api('/coupon/center', { token: token });
const c1 = (r.data.list || []).find(c => Number(c.id) === Number(c0.id));
chk(c1 && c1.received_by_me === true, '领券中心回显「已领取」');
sec('消息中心');
r = await api('/message/index', { token: token });
chk(r.code === 1 && r.data.list.length >= 1, '领券后自动生成消息', '共 ' + r.data.total + ' 条');
chk(r.data.unread >= 1, '未读数统计正确', 'unread=' + r.data.unread);
const msg = r.data.list[0];
r = await api('/message/read', { method: 'POST', token: token, data: { id: msg.id } });
chk(r.code === 1, '单条标记已读');
r = await api('/message/unread', { token: token });
chk(r.code === 1 && r.data.unread === 0, '未读数归零', 'unread=' + r.data.unread);
r = await api('/message/index?category=order', { token: token });
chk(r.code === 1, '按分类筛选可用', '订单类 ' + r.data.total + ' 条');
r = await api('/message/readAll', { method: 'POST', token: token, data: {} });
chk(r.code === 1, '全部标记已读接口可用', '影响 ' + r.data.count + ' 条');
r = await api('/message/index', { token: '', silent: true });
chk(r.code === 401 || r.code !== 1, '未登录访问消息被拦截', r.msg);
console.log('\n═══════════════════════════════════════════════════');
console.log(` 结果:通过 ${pass} 项 / 失败 ${fail} 项`);
if (fail) { console.log(' 失败明细:'); failures.forEach(f => console.log(' - ' + f)); process.exitCode = 1; }
else console.log(' 🎉 优惠券与消息中心链路通过');
console.log('═══════════════════════════════════════════════════');
})().catch(e => { console.error('\n💥 中断: ' + e.message); process.exitCode = 1; });

查看文件

@@ -56,6 +56,17 @@ export default {
reviewCreate: (data) => post('/review/create', data), reviewCreate: (data) => post('/review/create', data),
reviewList: (masterId) => get('/review/masterList', { master_id: masterId }), reviewList: (masterId) => get('/review/masterList', { master_id: masterId }),
/* ---------- 优惠券 ---------- */
couponCenter: () => get('/coupon/center'),
couponMy: (data) => get('/coupon/my', data),
couponReceive: (data) => post('/coupon/receive', data),
/* ---------- 消息中心 ---------- */
messageList: (data) => get('/message/index', data),
messageRead: (data) => post('/message/read', data),
messageReadAll: (data) => post('/message/readAll', data),
messageUnread: (data) => get('/message/unread', data),
/* ---------- 商家端 ---------- */ /* ---------- 商家端 ---------- */
merchantApply: (data) => post('/merchant/apply', data), merchantApply: (data) => post('/merchant/apply', data),
merchantInfo: () => get('/merchant/info'), merchantInfo: () => get('/merchant/info'),